본문 바로가기
해킹/시스템해킹

시스템해킹 메모리 커럽션

by 맑은청이 2020. 8. 2.
728x90
반응형

메모리 커럽션(Memory Corruption) , Corruption 은 구글에 치니 부패라는 뜻이 나오네요. 메모리를 오염시키는거니깐 대충 뜻이 맞는거 같습니다. 

https://com24everyday.tistory.com/224

 

시스템 해킹 기초

취약점의 분류 익스플로잇이란 사전적으로 악용, 취약점을 이용해 공격자가 의도한 동작을 수행하게 하는 코드 혹은 이를 이용한 공격 행위를 의미 소프트웨어 버그(Bug) : 프로그래머가 의도하�

com24everyday.tistory.com

 

 

저는 띠오리라는 보안 회사에서 만든 '드림핵'이라는 프로그램으로 공부를 하고 있습니다. 

https://dreamhack.io/

 

해커들의 놀이터, DreamHack

해킹과 보안에 대한 공부를 하고 싶은 학생, 안전한 코드를 작성하고 싶은 개발자, 보안 지식과 실력을 업그레이드 시키고 싶은 보안 전문가까지 함께 공부하고 연습하며 지식을 나누고 실력 향

dreamhack.io

1. 버퍼 오버플로우

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

 

C언어에서 버퍼란 지정된 크기의 메모리공간

발생 위치에 따라 스택 버퍼 오버플로우, 힙 오버플로우로 나뉨

이 들은 공격 방법이 다름

 

스택 버퍼 오버플로우는 다음과 같이 동작된다.

8 바이트 버퍼 A 와 8 바이트 버퍼 B 가 메모리에 할당되어있고 버퍼 A에 16바이트 데이터를 복사하면 버퍼 B 까지 씌여지게 된다.

 

이때를 버퍼 오버플로우가 발생했다고 하고 이는 프로그램의 Undefiend Behavior(런타임 중에 어떤 일이 발생할지 모르는 일)을 이끌어낸다.  

만약 함수 포인터를 B에 저장하고 있었으면 이상한 값으로 덮었다면 Segmentation Fault(접근 권한이 없는 메모리에 읽거나 쓰는 행위) 를 발생시킨다. 

 

버퍼 오버플로우 취약점은 프로그래머가 버퍼의 길이에 대한 가정을 올바르지 않게 하여 발생

길이 제한 없는 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));
	//strncpy 함수는 temp 버퍼를 복사할 때 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]))
		pritnf("Hello Admin!\n");
		else printf("Access Dened!\n");
}

temp 버퍼에 입력 받은 패스워드 복사 후 실제 패스워드인 "SECRET_PASSWORD" 문자열과 비교해서 같으면 auth 1 이고 이를 리턴한다. 허나 이때 16바이트보다 큰 값을 입력하면 temp 뒤에 있는 Auth 에 이상한 값이 들어가게 되는데 이러면 인증 여부와 상관없이 if(check_auth(argv[1]) 가 늘 참을 반환한다. 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{-----------redeacted---------}");
    }
}

고정된 크기의 버퍼보다 더 긴 데이터를 입력받아서 스택 버퍼 오버플로우가 발생. size를 넘어 win 까지 메모리를 조작해야한다. 

 

 

#include <stdio.h>

int main(void){
	char buf[32] = {0,};
	
	read(0,buf,31);
	sprintf(buf,"Your Input is: %s\n",buf);
	//31바이트를 가득 채운다면 sprintf 가 buf 에 쓸 때 버퍼 오버플로우가 발생함 
	puts(buf);
}

sprintf 로 버퍼에 다시 내용을 쓸 때 발생 32바이트를 다 쓰지 않았음에도 "Your Input is: " 때문에 안됨.

이러한 문제들 때문에 프로그래머가 길이에 대한 검증을 확실히 수행해야하고 버퍼에 저장하는 코드가 버퍼를 초과하지 않는지  신경써야함.

길이 제한가 없는 함수이면 잠재적으로 취약   

힙 오버플로우는 나중에 배움 

2. Out-of-boundary

 

/*
OOB는 버퍼의 길이 범휘를 벗어나는 인덱스에 접근할 때 발생하는 취약점  
*/
#include <stdio.h>

int main(void){
	int win;
	int idx;
	int buf[10]; //길이 10, 인덱스 가능 0~9  
	
	printf("Which index? ");
	scnaf("%d",&idx);
	//유효한 인덱스 입력을 받는지 검사하지 않 음  
	
	printf("Value: ");
	scanf("%d",&buf[idx]);
	
	printf("idx: %d, value: %d\n",idx,buf[idx]);
	
	if(win == 31337){
		printf("Theori{----redeacted-----}");
	}
}

 

 

OOB를 예방하기 위해서는 다음과 같은 범위 검사 장치를 넣어두어야함.

유효한 인덱스 검사

// 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{-----------redeacted---------}");
    }
}

다음과 같이 idx 를 %10 해서 0~9 까지의 수로 변환시켜준다고 생각할 수 있지만 이때는 음수를 고려 못한 거

 

그럼 음수을 경우 - 를 붙어서 해결

//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{-----------redeacted---------}");
    }
}

 하지만 이때는 -pow(2,31) 를 이용하면 뚫을 수 있음.

 

c언어에서 int 형으로 표현 가능한 정수 범위는 -pow(2,31)~pow(2,31)-1 이기 때문에 -를 붙인 pow(2,31)를 표현할 수가 없음. 결국 이는 표현 가능한 정수의 최댓값에서 1이 더 크기 때문에 -pow(2,31)과 동일한 값이 된다. 

근본적으로 OOB를 막기 위해서는 idx 를 unsigned int 로 선언하거나 if(idx < 0 || idx >= 10) 과 같은 경계 구문을 추가해야함

 

 

3. Off-by-one

#include <stdio.h> 

void copy_buf(char *buf,int sz){
	char temp[16];
	int i;
	for(i = 0; i <= sz; i++) //<= 을 사용하면 버퍼의 경계 계산에 맞지 않는다. 
		temp[i] =buf[i];//sz 번이 아닌 sz+1 번 반복하게 된다. 
		//off-by-one 
}

int main(void){
	char buf[16];
	read(0,buf,16);
	copy_buf(buf,sizeof(buf));
}

 

4. Format String Bug

포맷스트링 버그는 printf나 sprintf 와 같은 포맷 스트링을 사용하는 함수에서 발생하는 취약점 "%s" 처럼 프로그래머가 지정한 문자열이 아닌 사용자의 입력이 포맷 스트링으로 전달될 때 발생하는 취약점

일반 문자를 버퍼에 입력하는 게 아니고 "%x %s" 같은 포맷 스트링을 전달했을 때는 printf("%x %s") 형식으로 들어가기 때문에 즉 두번째 인자와 세번째 인자가 전달되지 않았기 때문에 쓰레기 값을 인자로 취급해 출력합니다. 

 

포맷 스트링 버그는 포맷 스트링을 사용하는 함수의 인자만 잘 검토하면 되기 때문에 다른 취약점들에 비해 막기 쉬움 특히 최신 컴파일러에서는 포맷 스트링으로 전달되는 인자가 문자열 리터럴(소스코드의 고정된 값) 이 아닐 경우 경고 메시지를 출려겨한다. 

 

하지만 프로그램에 큰 영향을 줄 수 있는 취약점이니 염두해 두어야한다. 

표준 c 라이브러리에서는 포맷 스트링을 사용하는 대표적인 함수들은 아래와 같다.

 

드림핵 연습 문제에서는 16진수의 flag 를 출력하는 문제였음.

답은 다음과 같이 포맷 스트링을 계속 출력해서 메모리 상에서 출력되게 하는 것.

처음 생각에는 buf 에 껄 그대로 출력하니깐 %x",flag);//

이렇게 넣으면 printf("%x",flag);//"); 이런 식으로 실행되지 않을까 싶었지만 예제 파일에서는 실행이 되지 않음.

질문을 해야겠음.

5. Double Free & Use After Free

C언어는 프로그래머가 수동으로 동적 메모리를 관리해야하는 저수준 언어이기 때문에 관리를 모새헛 해제된 메모리에 접근하거나 메모리를 할당하고 해제하지 않아 메모리 릭이 발생할 경우 큰 문제로 이어질 수 있음. 

동적 메모리 관리에서 가장 자주 발생하는 문제는 해제된 메모리를 정확히 관리하지 않아 발생하는 문제.

특히 Double Free 취약점은 이미 해제된 메모리를 다시 한번 해제하는 취약점이다. 

또 Use-After-Free(UAF) 취약점은 해제된 메모리에 접근해서 값을 쓰는 취약점이다. 

// df-1.c
#include <stdio.h>
#include <malloc.h>
int main(void) {
    char* a = (char *)malloc(100);
    char *b = (char *)malloc(100);
    memset(a, 0, 100);
    strcpy(a, "Hello World!");
    
    memset(b, 0, 100);
    strcpy(b, "Hello Pwnable!");
    
    printf("%s\n", a);
    printf("%s\n", b);
    
    free(a);
    free(b);
    
    free(a);//이미 해제도니 메모리 다시 해제  
    //예상치 못한 프로그램 흐름을 만들 수 있다.  
}
// uaf1.c
#include <stdio.h>
#include <string.h>
#include <malloc.h>
int main(void) {
    char *a = (char *)malloc(100);
    memset(a, 0, 100);
    
    strcpy(a, "Hello World!");
    printf("%s\n", a);
    free(a);
    
    char *b = (char *)malloc(100); 
    strcpy(b, "Hello Pwnable!");
    printf("%s\n", b);
    
    //여기서의 메모리 상황
    strcpy(a, "Hello World!");
    printf("%s\n", b);
}

이미 해제된 포인터를 호출해서 출력하는 모습을 볼 수 있다. 

보이는 것과 같이 포인터 a 와 포인터 b 가 가리키는 주소가 같다는 것을 알 수 있다. 이미 해제되었던 메모리 a 가 메모리 할당자로 들어가고 새로운 메모리 영역을 할당할 때 메모리를 효율적으로 관리하기 위해 기존에 해제되었던 메모리가 그대로 반환되어 일어나는 일이다. 

즉 이미 해제된 메모리를 재사용해서 다른 곳에서 사용되고 있는 메모리에 데이터가 작성될 수 있음 이를 Use-After-Free(UAF) 취약점이라고 한다. 

 

프로그램 규모가 커지거나 구조가 복잡할 수록 UAF 취약점은 생각이도 못한 곳에서 발생. 여러 컴포넌트들이 결합되면 더더욱 두드러짐. 한 컴포넌트에서는 객체나 메모리의 사용이 끝났다고 판단해 해제했지만 다른 컴포넌트에서는 이 내용이 동기화되지 않아 포인터를 그대로 사용할 수 있다. 

 

또 이런 취약점은 영향력을 판단하기 어려움 공격이 불가능하다고 알려진 버그가 실제로는 공격 가능했던 취약점인 경우도 있었음. 

 

해결법은 각 메모리 할당자의 구현을 정확히 알아야함 

6. 초기화 되지 않은 메모리

C와 C++ 에서는 수많은 구조체들과 클래스들을 선언하고 이들의 인스턴스( 실제로 저장공간에 클래스의 구조로 할당된 실체) 를 생성할 때는 프로그래머가 의도한 경우를 제외하고 반드시 초기화를 해야한다. 하지 않으면 쓰레기 값이 들어가고 프로그램의 흐름을 망칠 수 있다. 

 

공격자가 메모리를 정교하게 조작해 초기화되지 않은 영역에 공격자의 입력이 들어갔다고 생각해보자. 초기화 할 줄 알았는데 안 한 메모리를 보안 취약점으로 이어질 수 있다. 

첫번째는 name에 할당된 메모리를 초기화하지 않았다는 것.

read 함수는 입력 받을 때 널 바이트와 같은 별도의 구분자를 붙이지 않기 때문에 name 출력 부분에서 초기화 되지 않은 다른 메모리가 출력될 수 있다. 

 

두번째는 name_len 변수의 값이 100보다 크거나 같은 경우에 대한 예외 처리가 없다는 것.

이 경우 p.name 은 malloc 으로 할당된 값이 아니라 쓰레기 값이 된다. 이러면 공격자가 read 함수에서 원하는 메모리 주소에 원하는 값을 쓸 수 있게 된다. 

7. Integer issues

C나 C++ 언어를 사용할 때 자주 발생하는 취약점들 중 하나는 정수의 형 변환을 제대로 고려하지 못해 발생하는 취약점. 

특히 정수의 범위에 대한 정확한 이해 없이 작성된 코드는 자주 문제를 일으키게 된다. 즉 범위를 정확하게 알아야함. 

 

묵시적 형 변환

연산의 피연산자의 자료형이 서로 다를 경우 다양한 종류의 형변환이 일어나게 된다. 

직접 자료형을 명시해주지 않으면 묵시적으로 형 변환이 발생. 

정확히 숙지하지 않으면 이는 취약점으로 이어질 수 있다. 

 

묵시적  형 변환에 대한 규칙

- 대입 연산의 경우 작은 자료형에 큰 정수 저장될 경우 작은 정수의 크기에 맞춰 상위 바이트 소멸

- 정수 승격 char , short 같은 자료형이 연산될 때 일어난다. 컴퓨터가 int 형을 기반으로 연산하기 때문에 일어남

- 피연산자가 불일치할 경우 형 변환이 일어남. int < long < long long < float < double < long double 순으로 변환, 작은 바이트에서 큰 바이트로, 정수에서 실수로 형 변환이 일어나게 됨. 

ex) int 와 double 더하면 int가 double 형으로 변환된 후 연산 진행 

// int-1.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char *buf;
    int len;
    
    printf("Length: ");
    scanf("%d", &len);
    
    buf = (char *)malloc(len + 1);
    
    if(!buf) {
        printf("Error!");
        return -1;
    }
    
    read(0, buf, len);
}

len 값을 사용자에게 입력 받은 후 이후 len + 1 만큼 메모리를 할당받고 포인터를 buf 에 저장

그리고 read 함수를 통해 데이터를 len 만큼 buf에 입력 받는다. 

 

만약 len 에 -1 을 들어가게 되면  buf = (char*) malloc(0) 이 호출되고 

read(0,buf,-1)이 호출된다. 

인자로 전달된 값은 int 형 값 -1 이고 read 함수의 세번째 인자는 size_t 형이므로 묵시적 형 변환이 일어난다. (usigned)

즉 read(0, buf, pow(2,32)-1) 이 호출된다. 그러므로 지정된 크기의 버퍼 넘는 데이터를 넣을 수 있기 때문에 힙 오버플로우가 발생한다.(buf는 0) 

 

 

// int-2.c
char *create_tbl(unsigned int width, unsigned int height, char *row) {
	unsigned int n;
	int i;
	char *buf;
	n = width * height;
	buf = (char *)malloc(n); //n 크기의 테이블을 할당한 후
	
	if(!buf)
		return NULL;
	for(i = 0; i < height; i++)
		memcpy(&buf[i * width], row, width);//복사 
        //하지만 unsigned int 형의 변수기 때문에 width * height 가 pow(2,32)를 넘어가면 의도하지 않은 값이 들어가게 된다. 
	return buf;
}

 

728x90
반응형