Dreamhack/개념 정리

드림핵(Dreamhack) 2강 - 버퍼 오버플로우

핏디 2021. 4. 2. 11:55
SMALL

*버퍼란?

> 지정된 크기의 메모리 공간

 

*버퍼 오버플로우 취약점이란?

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

> 인근 메모리 덮어씀(다른 프로그램의 변수, 파라미터, 제어에 관련한 데이터 등)

> 결과적으로 프로그램의 데이터 오염, 예기치 않은 프로그램의 제어, 메모리 접근 위반, 공격자가 원하는 코드를 실행하는 등의 문제 발생

 

*종류    

> 스택 버퍼 오버플로우, 힙 오버플로우 등

-> 위치에 따라 구분

why?

버퍼 오버플로우는 인접한 메모리를 오염시키는 취약점으로, 어떤 메모리를 오염시킬 수 있는지에 따라 공격 방법이 달라짐!


#include <stdio.h>
#include <string.h>
void func(char* arg) {
char buffer[4];
strcpy(buffer, arg);
printf(“%s\n”, buffer);
}
int main(int argc, char* argv[]) {
char* myName = argv[1];
func(myName);
}

(주의)

> 최신 버전은 GCC는 overflow 방지를 위해 SSP (Stack smashing protector) 기능이 내장되어 있음

> 예제에서의 overflow 동작을 보기 위해서는 컴파일시 -fno-stack-protector를 추가해야 함 

예) $ gcc overflow.c –fno-stack-protecto

정상 실행
buffer overflow

> segmentation fault -> main으로 돌아갈 ebp에 문제가 생긴것!


#include <stdio.h>
#include <string.h>
void func(char* arg) {
int authenticated = 0;
char buffer[4];
if(strcmp(arg, “lim”) == 0) {
authenticated = 1;
}
strcpy(buffer, arg);
if(authenticated) {
printf(“Authenticated %s\n”, buffer);
} else {
printf(“Not authenticated %s\n”, buffer);
}
}
int main(int argc, char* argv[]) {
char* myName = argv[1];
func(myName);
}
  buf authenticated  

> 사실은 정상 동작이 아님! 어쩌다가 우연히 들어맞은것

> authenticated의 값에 e\0이 들어가면서 0보다 큰 값이 되어버림

-> c언어에서는 if문 속의 값이 0이면 거짓, 0보다 크면 참으로 판단함(파이썬에서는 true, false 정확하게 나눠서 씀)

-> 참으로 생각하여 인증이 되는 것임

 


안전하지 않은 C 함수

> 메모리 사이즈를 고려하지 않아 문제 발생

 

gets(char* str) 

> 표준 입력으로부터 str로 한 줄을 읽어 들인다

sprintf(char* str, char* format, …)

> format에 정의한 형식으로 str에 출력

strcat(char* dest, char* src)

> 문자열 dest뒤에 src를 이어 붙임

strcpy(char* dest, char* src)

> dest에 src의 문자열을 복사

 

안전하게 사용하기 위한 C 함수


코드 인젝션

> eip 주소를 \xff\xff\xce\xf0 으로 바꾸고자 함

> gcc –m32 –fno-stack-protector –z execstack –fno-pie –g overflow으로 컴파일


- 스택 버퍼 오버플로우

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

 

8바이트의 버퍼 A와 8바이트 데이터 버퍼 B가 선형적으로 할당됨.

if) 버퍼 A에 16 바이트의 데이터 복사 시, 데이터의 뒷부분은 버퍼 A를 넘어 뒤에 있는 데이터 영역인 B에 쓰여지게 됨.

-> 오버 플로우 발생 > Undefined Behavior 도출

if) B에 나중에 호출될 함수 포인터를 저장하고 있다면 이 값을 "AAAAAAAA" 와 같은 데이터로 덮었을 때 Segmentation Fault 발생할 것임. 이를 악용 시, 어딘가에 기계어 코드 삽입 후 함수 포인터를 공격자의 코드의 주소로 덮어 코드를 실행 가능.

 

*Undefined Behavior 이란?

> "정의 되어 있지 않은 동작", 런타임 중에 어떤 현상이 발생할 지 예측할 수 없음

*Segmentation Fault 란?

> 접근 권한이 없는 메모리 영역을 읽거나 쓰려고 할 때 발생하는 예외

 

예1) 입력 받은 데이터가 16바이트 이상일 경우

> 16 바이트 버퍼 buf 를 스택에 할당한 후, gets 함수를 통해 사용자로부터 데이터를 입력받아 출력하는 코드

> 길이 검ㅈ증이 없는 함수를 사용해 오버플로우 발생

 

-gets 함수 

> 사용자가 개행을 입력하기 전까지 입력했던 모든 내용을 첫 번째 인자로 전달된 버퍼에 저장하는 함수

> 별도의 길이 제한이 없어 16바이트가 넘는 데이터를 입력할 경우 스택 버퍼 오버플로우 발생

 

길이 제한이 없는 API 함수를 사용하거나 버퍼의 크기 보다 입력받는 데이터의 길이가 더 클 경우 자주 발생

 

버퍼를 오버플로우시켜 ret 영역을 0x41414141로 만듦

스택 기본 구조(출처 foxtrotin.tistory.com/351)

Buffer + sfp[4byte] + ret[4byte]

 

1) 버퍼: 데이터가 저장되는 공간

 

2) sfp: 스택 베이스값 (4byte)

sfp는 스택 주소값을 계산할 때 현재 스택값의 바닥, 기준을 잡을 때 필요한 프레임 포인터 값을 저장한다.

-sfp가 필요한 이유

ebp레지스터는 1개이기 때문에 함수가 시작할 때마다 ebp값이 바뀌는데, 그 전의 ebp값을 스택에다가 저장해야 하기 때문이다.

*ebp : ebp(Extended Base Pointer)는 StackFrame 의 시작 지점 주소가 저장되어 스택 복귀 주소로 사용된다. StackFrame 은 rbp 레지스터를 사용해 스택 내에 존재하는 지역변수, 파라미터, 복귀 주소에 접근하는 기법을 말한다.

 

3) ret 은 return의 약자로 반환 주소값을 뜻한다

-ret가 필요한 이유

다음에 실행해야 하는 명령이 위치한 메모리 주소값이 ret이기 때문이다. ret부분을 자기가 원하는 명령이 있는 곳의 메모리 주소로 덮어 쓴다면 자기가 원하는 명령을 실행시켜 버릴 수도 있다. 

예) 쉘을 실행하는 코드를 저장한 뒤에 그 메모리 주소를 ret부분에 쓰면 함수 종료 후에 쉘을 실행시키게 된다. 이 값이 eip값으로 변하고 다음 주소가 실행이 된다. 

*eip: 다음에 실행할 명령어의 주소를 가지고있는 레지스터이다. 현재 실행하고있는 명령어가 종료되면 EIP 레지스터에있는 명령어를 실행하게 된다.

 

예2) 스택 버퍼 오버플로우

line 10 ) password 의 길이 만큼 temp에 복사.              line 12) temp  와  SECRET_PASSWORD  를 비교

> main() 함수 : argv[1]을 check함수의 인자로 전달 후 리턴 값을 받아옴. 리턴 값이 0이 아니면 "Hello Admin!", 0이라면 "Access Denied!"라는 문자열 출력함.

>check_auth() 함수 : 16바이트 크기의 temp 버퍼에 입력받은 패스워드를 복사한 후 "SECRET_PASSWORD"문자열과 비교. 문자열이 같다면 auth 변수를1로 설정하고 auth 리턴. strncpy(temp, password, strlen(password)); 에서 temp 버퍼를 복사할 때, temp의 크기(16바이트)만큼이 아닌 인자로 전달된 password 문자열의 길이만큼을 복사함. 이에 따라 argv[1]에 16바이트 이상의 문자열 전달시, 길이 제한 없이 문자열이 복사되는 문제점 발생 --> 스택 버퍼 오버플로우 발생

temp 버퍼 뒤에 auth 값이 존재하므로, 오버플로우가 발생해 공격자의 데이터가 auth 값을 0이 아닌 다른 값으로 변경할 수 있음. 이 경우 실제 인증 여부와 관계없이 if (check_auth(argv[1]))은 항상 참을 반환하게 됨.

temp는 16바이트, auth는 int형으로 4바이트가 할당된 것을 확인할 수 있음.

예3) 

> main() 함수 : 24 바이트의 크기의 버퍼 buf 할당. scanf() 함수를 통해 size 변수에 값을 입력받고, size 만큼 buf에 데이터 입력받음. (line 6~8)

> 고정된 크기의 버퍼보다 더 긴 데이터를 입력받아 오버플로우 발생

 

예4)

> line 5-7 : 32 바이트 크기 buf를 초기화 한 후, 데이터를 31 바이트만큼 입력받음

> line 8 : sprint() 를 통해 출력할 문자열을 저장 후 출력

> read()에서 받는 입력이 32바이트를 넘지 않지만 buf에 31바이트를 꽉 채운다면, sprint()를 통해 버퍼에 값을 쓸 때 "Your Input is: "문자열을 추가하기 때문에 총 길이가 32바이트를 넘는 경우가 발생함 --> 오버플로우 발생

 

-힙 오버플로우

> 스택 버퍼 오버플로우와 메모리 영역 차이만 존재, 원인은 같음. 그러나 익스플로잇하는 방법은 스택영역과 다르게 시도해야함.

 

예1)

> line 6-7 : 40바이트 크기의 힙 버퍼 input과 hello 할당

> line 12 : hello 버퍼에 "Hi!" 문자열을 복사

> line13 : read 함수를 통해 input에 데이터 입력. 

> read() 함수를 통해 입력받는 길이인 100 바이트가 input 버퍼의 크기인 40 바이트보다 크기 때문에 힙 오버플로우 발생

 

힙 오버플로우 발생 시, 힙 메모리 상태

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

 

*memset() : 메모리의 내용(값)을 원하는 크기만큼 특정 값으로 세팅할 수 있는 함수

- memory + setting 메모리를 (특정 값으로) 세팅

- 사용 형태

> void* memset(void* ptr, int value, size_t num);

void* ptr은 세팅하고자 하는 메모리의 시작 주소. 그 주소를 가리키고 있는 포인터가 위치하는 자리 

value는 메모리에 세팅하고자 하는 값. int 타입으로 받지만 내부에서는 unsigned char 로 변환되어서 저장됨. 즉 'a' 등 문자열을 넣어도 무방

size_t num은 길이. 이 길이는 바이트 단위로써 메모리의 크기 한조각 단위의 길이. "길이 * sizeof(데이터타입)" 의 형태로 작성하면 됨.

반환값은 성공-> 첫번째 인자로 들어간 ptr 반환, 실패-> NULL 반환.

예) 배열 초기화 시, for문과 더불어 memset()을 사용할 수 있음.


출처: https://blockdmask.tistory.com/441 [개발자 지망생]

 

* read(): 열려있는 파일을 읽는 함수, scanf() 와 같이 입력을 받을 수도 있음.

 

 

<Summary>

- 버퍼 오버플로우는 프로그래머가 길이에 대한 검증을 정확히 수행하지 못해 발생함.

- 공격 벡터로부터 데이터를 입력받고 이를 버퍼에 저장하는 코드가 있다면 유의 해야함.

- 데이터를 버퍼에 입력 받는 경우, 입력 받은 데이터가 버퍼의 범위를 초과해서는 안됨.

- 입력 받을 때, 길이 제한이 없는 함수 사용 시 잠재적으로 취약함. (get() 함수 등) 

> 입력 받은 데이터가 버퍼에 저장되기까지의 흐름을 파악해 버퍼의 크기를 넘는 양을 저장할 수 있는지 검토 필요.

 

 

 

 

 

 

LIST