시스템 해킹 실습 -4 . Uninitialized 미초기화 취약점

2024. 7. 18. 11:11Information Security 정보보안/Vulnerability Analysis 취약점 분석

728x90

배울내용: 

시스템 해킹

단위공격 기술

미초기화 발생 원리

Uninitialized 실습 

Uninitialized    방지 방법

메모리 취약점

미초기화 취약점 이란? 

 

 

 

 

사진 출처 : https://payatu.com/blog/uninitialized-stack-variable/

 

 

 

 

 

Uninitialized 란? 

 

초기화되지않은 상태에서 접글하려는 상태

 

 

이 취약점은 소프트웨어 개발에서 발생하는 흔한 보안 취약점 중 하나다.

이는 프로그램이 메모리의 변수 또는 데이터 구조를 초기화하지 않고 사용하는 경우에 발생한다.

 

 

 

주요 개념

  1. 초기화 (Initialization):
    • 변수를 선언할 때 처음 값을 설정하는 것을 의미.
    • 예: int x = 0;
  2. 미초기화 변수 (Uninitialized Variable):
    • 변수가 초기화되지 않은 상태에서 사용되는 경우를 의미.
    • 예: int y; printf("%d", y); (여기서 y는 초기화되지 않음)

 

 

 

 

 

왜 위험한가?

  1. 예측 불가능한 동작:
    • 초기화되지 않은 변수를 사용할 때, 해당 변수에는 이전에 메모리에 저장된 임의의 값이 들어 있을 수 있다.
    • 이는 프로그램이 의도하지 않은 값을 사용하게 되어 예측하지 못한 결과를 초래할 수 있다.
  2. 보안 취약점:
    • 공격자는 미초기화 변수를 악용하여 메모리 내용을 노출시키거나 권한 상승 등의 공격을 수행할 수 있다.
    • 예를 들어, 암호화된 데이터나 중요한 정보를 포함하는 메모리 영역이 의도치 않게 노출될 수 있다.

 

 

 

 

 

 

 

 

코드작성

더보기

 

uninit

/*
    gcc ./uninit.c -o uninit
*/

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define ALLOC_COUNT 10

struct Data *allocates[ALLOC_COUNT] = {};

struct Data{
    uint8_t * ptr;
    uint64_t size;
};


void flushbuf(){
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

uint32_t findEmpty(){
    for(int i = 0; i < ALLOC_COUNT; i++){
        if(allocates[i] == NULL){
            return i;
        }
    }
    return -1;
}

void allocate(){
    int idx = findEmpty();
    if(idx < 0){
        printf("[-] No empty space\n");
        return;
    }
    struct Data * data = (struct Data*)malloc(sizeof(struct Data));
    allocates[idx] = data;
    printf("[+] index [%d] Allocated at %p\n",idx, data);
    return;
}

void insert(){
    uint32_t idx = 0;
    char buf[1024] = {};

    printf("select idx > ");
    scanf("%u",&idx);
    flushbuf();

    if(idx > 9 || allocates[idx] == NULL){
        printf("[-] Invalid index.\n");
        return;
    };
   
    struct Data * data = allocates[idx];
    if(data->ptr != NULL){
        printf("[-] Data Exists.\n");
        return;
    }

    printf("input > ");
    fgets(buf, 1024, stdin);
    uint32_t len = strlen(buf);
    buf[len - 1] = '\0';

    data->ptr = (uint8_t*)malloc(len);
    memcpy(data->ptr, buf, len);
    data->size = len;
    printf("[+] Data Inserted at [%d]\n", idx);
    return;
}

void readData(){
    uint32_t idx = 0;

    printf("select idx > ");
    scanf("%u",&idx);
    flushbuf();

    if(idx > 9 || allocates[idx] == NULL){
        printf("[-] Invalid index.\n");
        return;
    };
   
    struct Data * data = allocates[idx];
    if(data->ptr == NULL){
        printf("[-] Data not Exists.\n");
        return;
    }
    printf("Data > %s\n\n", data->ptr);
    return;
}

void deallocate(){
    uint32_t idx = 0;

    printf("select idx > ");
    scanf("%u",&idx);
    flushbuf();

    if(idx > 9 || allocates[idx] == NULL){
        printf("[-] Invalid index.\n");
        return;
    };
   
    struct Data * data = allocates[idx];
    if(data->ptr != NULL){
        free(data->ptr);
    }
    free(data);
    allocates[idx] = NULL;
    printf("[+] [%d] deallocated\n", idx);
    return;
}

void printSpace(){
    printf("\n[Space]\n");
    for(int i = 0 ; i < ALLOC_COUNT; i++){
         if(allocates[i] != NULL){
            printf("[%d] ", i);
        }
    }
    printf("\n");
    printf("\n");
}


int menu(){
    printSpace();

    printf("[MENU]\n");
    printf("1. Allocate\n");
    printf("2. Insert\n");
    printf("3. Read\n");
    printf("4. Deallocate\n");
    printf("0. Exit\n");
    printf("> ");

    int idx = 0;
    scanf("%u",&idx);
    flushbuf();
    switch(idx){
        case 1:
            allocate();
            break;
        case 2:
            insert();
            break;
        case 3:
            readData();
            break;
        case 4:
            deallocate();
            break;
        case 0:
            exit(0);
            break;
        default:
            break;
    }
}

int main(){
    printf("Hello. This is Simple Allocator.\n\n");
    while(1){
        menu();
    }
}

우분투를 켜주고 하나의 파일 위치에 위에 2개의 파일을 만들고 c 파일은 컴파일 시켜준다

 

 

 

컴파일

gcc -c uninit.c -o uninit

 

 

 

그러면 아래와 같이 초록색 파일이 나오는데 ./uninit 로 실행해준다

 

 

 

 

 

 

 

이번에는 gdb 를 확인하지않고는 문제를 풀수가 없다 

그러한 이유는 변수를 초기화 할떄 초기화 되지 않을걸 증명하려면 초기화 되는 주소를 봐야하기 떄문이다 

 

 

 

 

GDB(GNU Debugger) 프로그램의 디버깅을 위해 사용되는 강력한 도구 로  gdb uninit 로  디버깅 해보자 

 

 그리고 disass main (disassemble main) 으로 main 함수를 어셈블링 시킬수있다 

이떄 터미널 창을 2개를 뛰워 코드와 함께 봐주면 된다 

 

 

 

그전에 한번 소스코드를 보고어떻게 작동하는지 파악하고 

실행해보고 

그리고 나서 구조를이해한 상태에서 디버깅을 해본다 

 

 

 

 

 

실제로 uninit 파일의 위치로 가서 ./uninit 을 실행하면 위와 같이 보이게 된다 

 

 

그리고 소스코드를 다확인해보게 되면

 

 

위에서 어떻게 되는지 코드를 보면서 이해할수가있다 

 

 

 

1번을 클릭하면

 

 

 

index 0 위치에 allocate 되었다고 되어있고 그 뒤에 주소가 나온다

 

 

2번을 클릭하면

 

 

 

Allocate 되었던 인덱스를 넣어주고 그안에 input 안에 원하는 글 을 넣으면  0위치에 sarimus 가들어가는걸 볼수있다

 

 

 

3번을 클릭하면 

 

 

인덱스의 위치를 알려주면 아까 작성했던 "sarimus" 가출력이 된다 

 

 

 

 

4번을 클릭하면 

 

 

원하는 인덱스의 주소를 deallocate 시켜주고 

 

 

0번은 종료이다 

 

 

그러나 우리가 알아야할것은 여기서 

uninitialized 취약점이다 . 즉, 이미 할당했던곳에 다시 할당해버리면 나오는 취약점이라고 생각하면된다

그러면 현재 0 위치에 할당해서 free 시켜주고 다시 할당을 하면 어떻게 될까?

 

 


아까와 같이 1~3번을 진행하려했는데 1번에서는 똑같은 주소인 "0x5cba8a747ac0" 에다가 할당한게 보이지만

2번을 했을때는 이미 데이터가 있다고 나온다 

그리고 3번을 눌러 idx 위치가 0 인걸 읽을려고 하면 Segmentation fault (core dumped) 라는 에러가 나오면서 종료 된다 

 

 

 

gdb 로 실행해서 Segmentation fault 가 뜨면 그상태에서 br (back trace) 로 이전에 실행했던걸 역으로 따라가보면서

어디서 잘못된지를 찾을수가 있다

 

 

그렇게 찾다보면 주소에 값이 없는데 주소등록해서 segmentation error 뜨는걸 볼수있음

 

 

 

풀이

 

메모리 할당 및 동적 메모리 관리에서 발생할 수 있는 문제

동적 메모리 할당과 관련된 문제를 순서대로 정리를 해봤다 .

1. 초기 메모리 할당

먼저, 프로그램이 실행되면 동적 메모리 할당을 수행.

// 메모리 할당 예시 
struct Data* data = (struct Data*) malloc(sizeof(struct Data));
//할당 후, 메모리 주소를 확인합니다.
 
x/2gx 0x5555559ac0

이 명령어로 할당된 메모리 주소의 내용을 확인했을 때, 초기값은 0 이다. 

 

2. 데이터 삽입

데이터를 구조체에 삽입 한뒤에 

// 데이터 삽입 예시
strcpy(data->ptr, "aaa");
//다시 메모리 주소를 확인합니다.
 
x/2gx 0x5555559ac0

이 명령어로 값을 확인했을 때, 삽입한 데이터가 반영된 것을 볼 수 있다.

 

그리고 그림으로 보면 아래와 같다

 

       +----------------+
data ->|  ptr --------+ | 
       +----------------+
                       |
                       v
       +----------------+
ptr -> |    "aaa"     | 
       +----------------+

 

3. 데이터 읽기

그리고 데이터를 읽을 때는 data->ptr를 통해 접근한 후

 
// 데이터 읽기 예시
printf("%s\n", data->ptr);
       +----------------+
data ->|  ptr --------+ | 
       +----------------+
                       |
                       v
       +----------------+
ptr -> |    "aaa"     | 
       +----------------+

 

 

4. 메모리 해제

메모리를 해제한다.

// 메모리 해제 예시
free(data);
//해제 후, 다시 메모리 주소를 확인합니다.
 
       +----------------+
data ->|   Free Block   | 
       +----------------+
       (메모리 해제 이후에는 임의의 데이터가 있을수있다)
 
 

다시 x/2gx 0x5555559ac0

이 명령어로 값을 확인했을 때, 이상한 데이터가 들어가 있을 수 있다. 이는 메모리가 해제되었기 때문이다.

 

해제된 메모리의 상태

해제된 메모리는 재사용될 수 있는 상태가 되며, 더미 데이터(예: 0xDEADBEEF)가 들어가 있을 수 있다.

 

 

5. 재할당 및 초기화

메모리를 다시 할당하면 이전에 해제된 메모리를 사용할 수 있다.

// 메모리 재할당 예시
data = (struct Data*) malloc(sizeof(struct Data));
//할당 후, 메모리 주소를 확인합니다.

 

 

x/2gx 0x5555559ac0

초기 상태를 보면, 첫 번째는 더미 데이터, 두 번째는 0이 될 수 있는데 이는 이전에 해제된 메모리 상태를 반영.

 

       +----------------+
data ->|  ptr --------+ | <- malloc(sizeof(struct Data))
       +----------------+
                       |
                       v
       +----------------+
ptr -> |     ????      |  <- malloc(4)
       +----------------+
       (이전에 할당한게 메모리에 임의의 데이터가 들어가있을수있음)

 

 

 

 

6. 포인터 관련 문제

구조체 data를 보면, ptr이 첫 번째 멤버. 하지만 초기화되지 않은 경우, 잘못된 주소를 참조할 수 있다.

// 잘못된 주소 참조 예시
if (data->ptr == NULL) { // 오류 처리 }
이때, data->ptr에서 오류가 발생할 수 있습니다.​

 

이는 포인터가 올바른 주소를 가리키지 않기 때문이다. 그림으로 보면 아래와 같다 

그림으로 보면 위와 같다 

       +----------------+
data ->|  ptr --------+ | 
       +----------------+
                       |
                       v
       +----------------+
ptr -> |   ????       |  <- 유효하지않는 메모리주소가 들어가있을수있음
       +----------------+
       (메모리에 임의의 데이터가 있어서 에러를 유발할수있다 )

 

 

 

 

 

결론

이 예제는 동적 메모리 할당 시 항상 초기화해야 하는 이유를 설명한다. 초기화되지 않은 메모리는 이상한 데이터를 포함할 수 있으며, 이는 포인터와 관련된 코드에서 큰 문제를 일으킬 수 있다. 

 

 

 

예방 방법

 

       +----------------+
data ->|  ptr --------+ | <- 올바르게 초기화됨
       +----------------+
                       |
                       v
       +----------------+
ptr -> |     0x0000    | <- 올바르게 초기화된 메모리
       +----------------+
  1. 변수를 선언할 때 초기화하기:
    • 변수 선언 시 항상 초기값을 지정한다.
    • 예: int x = 0;
  2. 정적 분석 도구 사용:
    • 코드에서 미초기화 변수 사용을 검출하는 정적 분석 도구를 활용한다.
    • 예: Coverity, PVS-Studio, clang의 scan-build 등
  3. 코드 리뷰 및 테스트:
    • 철저한 코드 리뷰와 테스트를 통해 미초기화 변수 사용을 방지한다.

 

728x90