버퍼 오버플로(Buffer Overflow)
프로세스 메모리 구조
- 프로세스의 메모리 구조는 Text, Data, Heap, Stack 영역으로 구분되어 있음
- Text 영역 : 프로그램 코드와 상수가 정의되어 있고, 읽기만 가능한 메모리 영역이기 때문에 데이터를 저장하려고 하면 분할 충돌을 일으켜 프로세스가 중지됨
- Data 영역 : 전역 변수(Global Variable)와 정적 변수(Static Variable)가 저장된 영역
- Heap 영역 : 프로그래머의 필요에 따라 동적 메모리 호출로 할당되는 메모리 영역
- Stack 영역 : 함수 인자 값, 함수 내의 지역 변수, 함수의 반환 주소 등이 저장되는 영역으로 상위 메모리 주소에서 하위 메모리 주소로 데이터가 저장됨
개요
버퍼 또는 데이터 저장 영역에 할당된 용량보다 더 많은 입력이 위치하면 다른 정보를 변경할 수 있는 조건임. 공격자는 이런 조건을 이용하여 시스템을 중지시키거나 시스템의 제어를 갖기 위한 특별한 코드를 삽입함
버퍼 오버플로는 프로세스가 정해진 크기의 버퍼 한계를 벗어나 이웃한 메모리 위치에 데이터를 겹쳐 쓰려고 시도하는 것과 같은 프로그래밍 오류의 결과로 발생함
공격 원리
#include <stdio.h>
int main(int argc, char *argv[]){
char buffer[10]; // 10바이트 크기의 문자 배열 선언
strcpy(buffer, argv[1]); // 버퍼에 argv[1] 복사
printf("%s\\n", buffer); // 버퍼에 저장된 문자열 출력
}
스택 오버플로(Stack Overflow)
개요
스택 오버플로 공격은 보통 SetUID(Set User ID)가 설정된 루트 권한의 프로그램을 공격 대상으로 함. 스택에 정해진 버퍼보다 큰 공격 코드를 삽입하여 반환 주소를 변경함으로써 임의의 공격 코드를 루트 권한으로 실행하도록 하는 방법임
1988년 모리스 인터넷 웜에서 처음 발견된 이후 계속 이용됐음. 이것은 fingerd 데몬에서 사용한 gets( ) 함수의 검사되지 않은 버퍼 오버플로를 이용하였음
셸 코드
많은 버퍼 오버플로 공격의 핵심 요소는 오버플로가 발생하는 버퍼에 저장되는 공격자의 코드로 실행 제어를 이동시키는 것임
사용자 명령어 라인의 해석기인 셸로 제어를 넘기고 공격당한 프로그램의 권한으로 시스템의 다른 프로그램에 접근하기 때문에 셸 코드라고 함
공격 절차
1단계 : 공격 셸 코드를 버퍼에 저장
2단계 : 루트 권한으로 실행되는 프로그램의 특정 함수의 스택 반환 주소 버퍼를 오버플로 시켜서 공격 셸 코드가 저장된 버퍼의 주소로 덮어씌움
3단계 : 특정 함수의 호출이 완료되면 조작된 반환 주소로 셸 코드의 주소가 반환되어 셸 코드가 실행되고, 루트 권한을 획득하게 됨
힙 오버플로(Heap Overflow)
개요
힙은 프로그램이 실행하면서 동적 메모리 할당을 위해 사용하는 영역을 말하는데 일반적으로 프로그램과 전역 데이터 위에 위치하며 메모리 위 방향으로 커짐(스택은 아래 방향)
힙에 요청되는 메모리는 레코드의 연결 리스트와 같은 동적 데이터 구조를 위해 사용됨. 만약 이런 레코드가 오버플로에 취약한 버퍼를 가지고 있다면 연속된 메모리가 손상될 수 있음
스택과는 다르게 실행 제어를 쉽게 이동시킬 수 있는 반환 주소는 없음. 그러나 할당된 공간이 함수에 대한 포인터를 포함하고 있다면 공격자는 이 주소를 변경하여 겹쳐 쓴 버퍼에 있는 셸 코드를 가리키도록 할 수 있음
공격 절차
1단계 : 공격 셸 코드를 힙 버퍼에 저장
2단계 : 버퍼 오버플로 시켜서 인접한 포인터나 메모리 영역을 덮어씌움
3단계 : 덮어쓴 포인터를 공격 셸 코드가 저장된 메모리 주소로 변경하여 해당 포인터가 셸 코드를 실행하도록 유도하고, 루트 권한을 획득하게 됨
대응책
컴파일 시간 방어 : 새 프로그램 내에서 공격을 저지하도록 프로그램을 강화하는 것을 목표로 함
실행 시간 방어 : 존재하는 프로그램에서 공격을 발견하고 중지시키는 것을 목표로 함
컴파일 시간 방어
기본 개념
- 프로그램을 컴파일할 때 검사하여 버퍼 오버플로를 방지하거나 발견하는 것을 목표로 함
- 가장 기본적인 대응책은 버퍼 오버플로를 허용하지 않는 Java, ADA, Python과 같은 현대화된 고급 프로그래밍 언어를 사용하여 소프트웨어를 작성하는 것임
- C나 C++과 같이 메모리 접근에 높은 자유도를 가진 프로그래밍 언어를 사용하는 경우 입력되는 데이터의 크기가 할당된 버퍼의 크기를 초과하지 않도록 반드시 확인하는 로직을 포함하여 안전하게 프로그래밍해야 함
프로그래밍 언어의 선택
- 현대의 고급 수준의 프로그래밍 언어를 사용하여 프로그램을 작성하는 방법이 있음. 이런 언어는 변수 타입과 그 타입에 허용되는 연산들에 대해 강력한 표기법을 제공함
- 컴파일러가 범위 검사를 강제로 수행하는 코드를 자동으로 추가하기에 프로그래머는 명시적으로 그것을 코딩할 필요 없기 때문에 버퍼 오버플로 공격에 취약하지 않음
안전한 코딩 기법들
- 프로그래머는 코드를 검사하고 불안전한 코딩 구조를 안전한 방법으로 재작성할 필요가 있음
- 사용을 자제해야 하는 함수 : strcat( ), strcpy( ), gets( ), scanf( ), sscanf( ), vscanf( ), vsscanf( ), sprintf( ), vsprintf( ), gethostbyname( ), realpath( )
- 사용을 권장하는 함수 : strncat( ), strncpy( ), fgets( ), fscanf( ), vfscanf( ), snprintf( ), vsnprintf( )
언어 확장과 안전한 라이브러리 사용
- 표준 문자열 라이브러리를 안전한 것으로 교체하는 방법이 있음
- 대표적으로 Libsafe가 있음. 표준 C 라이브러리의 strcpy, sprintf 등 문자열 함수들을 안전하게 구현하면서 추가로 복사 연산이 스택 프레임 내부의 지역 변수 공간을 넘어가지 않도록 검사하는 기능을 구현하였음
- 수정된 라이브러리는 최소한 표준 라이브러리만큼 효율적이라 알려져 있으며, 기존의 프로그램에 대한 버퍼 오버플로 공격을 쉽게 방어하게 해줌
스택 보호 메커니즘(Stack Guard)
- 스택 오버플로 공격에 대해 프로그램을 보호하는 효과적인 방법 중 하나는 함수의 진입(entry)과 종료(exit) 코드를 조사하고 함수의 스택 프레임에 대해 손상이 있는지를 검사하는 것임. 어떤 변경이 발견된다면 공격을 계속 허용하는 대신 프로그램을 종료시킴
- Stack Guard는 가장 잘 알려진 보호 메커니즘 중 하나임. GCC 컴파일러의 확장 버전으로 추가적인 함수 진입과 종료 코드를 삽입함
- 컴파일러가 프로그램의 함수 호출(프롤로그) 시에 ret 앞에 카나리아(Canary) 값을 주입하고, 종료(return, 에필로그) 시에 canary 값이 변조되었는지의 여부를 확인하여 버퍼 오버플로 공격을 탐지함
스택 쉴드(Stack Shield)
- 함수 시작 시 복귀 주소(Return Address)를 Global RET라는 특수 스택에 저장해 두었다가 함수 종료 시 저장된 값과 스택의 RET 값을 비교해 다를 경우 오버플로 가정하여 프로그램 실행을 중단시키는 기술을 말함
실행 시간 방어
기본 개념
- 대부분의 컴파일 시간 기법은 프로그램을 재컴파일해야 하므로, 기존의 취약한 프로그램을 보호하기 위한 대안으로 운영체제에서 배포할 수 있는 실행 시간 방어법이 인기를 끌고 있음
- 실행 시점 대응책은 실행 가능 주소 공간 보호(Executable Address Space Protection) 기법과 주소 공간 임의화(Address Space Randomization) 기법을 포함함
- 실행가능 주소 공간 보호 기법은 실행 코드가 프로세스 메모리 내 특정 위치에서만 실행되도록 제한함으로써, 공격자가 스택 버퍼에 주입한 실행 코드(셸 코드)를 원천적으로 실행될 수 없게 만듦. 대부분의 버퍼 오버플로 공격은 스택 버퍼에 실행 코드를 주입하는 방식으로 이루어지므로, 재컴파일 없이 기존 프로그램들을 보호할 수 있어 매우 효과적임
- 주소 공간 임의화 기법은 스택 버퍼가 위치하는 주소 공간을 메모리 내에서 임의로 배치함으로써, 공격자가 스택 버퍼에 주입한 실행 코드의 주소를 예측할 수 없게 만듦. 공격자가 반환 주소를 변경하는 데 어려움을 겪게 하여, 스택 버퍼 오버플로 공격을 매우 어렵게 만듦
주소 공간의 임의 추출(ASLR, Address Space Layout Randomization)
- 공격을 막는 다른 실행 시간 기법으로는 프로세스 주소 공간에 있는 중요 데이터 구조의 위치를 임의로 변경하여 조작하는 것이 있음
- 공격자는 셸 코드로 제어를 넘기기 위해 공격에 사용할 적당한 반환 주소를 결정하는데, 이때 주소를 예측하여 공격함. 각 프로세스의 스택, 힙, 라이브러리 등의 메모리 위치를 임의로 배치하여 공격자가 이 주소를 예측하기 어렵게 만듦
NOP sled(NOP 썰매)
- NOP(No Operation)는 아무 기능도 수행하지 않는 명령어(instruction)를 의미함. 대표적으로 많이 보이는 형태는 인텔 x86 시스템에서 사용하는 NOP 명령어로 기계어 코드의 헥사 값이 0x90임
- 프로그램에서 NOP 명령은 빈 공간을 채우기 위한 명령어로 해당 명령어를 만나면 아무런 동작 없이 다음 명령어로 넘어감
- 셸 코드 실행을 위해 NOP 명령을 삽입하는 이유는 공격자가 셸 코드로 제어를 넘기기 위해 정확한 반환 주소를 알아야 하는데 프로그램 실행 시마다 셸 코드의 주소가 조금씩 바뀔 수 있어 정확한 주소를 알기 어렵기 때문임
- 이를 해결하기 위해 다수의 NOP 명령을 셸 코드 앞에 삽입하여 반환 주소가 NOP sled에 들어가게 되면 실행 흐름이 NOP sled를 지나쳐 최종적으로 셸 코드가 실행되도록 하기 위함임
실행 가능 주소 공간의 보호
- 스택과 힙을 실행 불능(Non-Executable Stack)으로 설정하여 기존 프로그램에 대한 여러 유형의 버퍼 오버플로 공격을 방어하는 기법임. 최신 운영체제는 이 기법을 표준으로 포함하고 있음
- 프로그램이 실행 가능 스택을 사용하지 않도록 설정하는 방법으로 Solaris 2.7 이상의 버전에서는 /etc/system 파일에 다음 두 라인을 추가하여 버퍼 오버플로 공격을 방어할 수 있음
# 사용자의 스택을 실행 불가능하게 설정(버퍼 오버플로 공격 방어)
set noexec_user_stack = 1
# 스택 실행 불능 설정을 우회하려는 시도를 로그로 기록(모니터링 용도)
set noexec_user_stack_log = 1
'정보보안 > 시스템보안' 카테고리의 다른 글
시스템 보안위협 레이스 컨디션 공격 (0) | 2025.02.23 |
---|---|
시스템 보안위협 포맷 스트링 공격 (0) | 2025.02.22 |
서버 보안 서버 해킹 기법 (0) | 2025.02.20 |
서버 보안 SW 설치 및 운영 (0) | 2025.02.19 |
서버 보안 보안 도구 활용 (0) | 2025.02.18 |