본문 바로가기

Dreamhack/Lecture & Practice

[Lecture] OOB (Out of bounds)

1. 배열의 속성

  • 배열은 연속된 메모리 공간을 점유하며, 배열의 각 요소의 주소는 배열의 주소, 요소의 인덱스, 요소 자료형 크기를 이용하여 계산한다.
  • 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료형의 크기를 곱한 값이며, 배열이 포함하는 요소의 개수를 배열의 길이라고 한다.
// array[n]의 배열 크기
sizeof(array) = sizeof(elem) * n

// array[n]의 k번째 배열을 참조
&array[k] = array + sizeof(elem) * k

 

2. OOB 

2-1. 정의

  • 배열의 임의 인덱스에 접근이 가능하여, 악의적으로 배열의 경계를 넘어 값을 읽거나 쓸 수 있는 취약점이다.
  • 배열 참조에 사용되는 인데스를 임의 값으로 설정할 수 있다면, 배열의 주소로부터 특정 오프셋에 있는 메모리 값을 참조할 수 있다.

2-2. OOB가 발생하는 이유

  • 요소를 참조할 때, 인덱스 값이 음수이거나, 배열의 길이를 벗어날 때 발생한다.
  • 개발자가 인덱스의 범위에 대한 검사를 명시적으로 프로그래밍하지 않아 발생한다.
    즉, 계산한 주소가 배열의 범위 안에 있는지 검사하지 않는 것이다.

3. OOB PoC (Out of bounds Proof-of-Concept)

3-1. 실습 코드

// Name: oob.c
// Compile: gcc -o oob oob.c

#include <stdio.h>

int main() {
  int arr[10];
  
  printf("In Bound: \n");
  printf("arr: %p\n", arr);
  printf("arr[0]: %p\n\n", &arr[0]);
  printf("Out of Bounds: \n");
  printf("arr[-1]: %p\n", &arr[-1]);
  printf("arr[100]: %p\n", &arr[100]);
  
  return 0;
}

 

3-2. 실습과정 & 결과

  • arr 배열의 범위를 벗어나는 -1과 100을 인덱스로 사용했음에도 아무런 경고 없이 출력되는 것을 볼 수 있다.
  • arr[0]과 arr[100]주소 차이를 보면, 0x7ffe85050510 - 0x7ffe85050380 = 0x190이고, 0x190은 0x4 * 0x64(=100)이다.
    즉, 배열의 범위를 벗어난 인덱스를 참조할 때, 앞서 살펴본 식을 그대로 사용한다는 점을 확인할 수 있다.

실행한 결과

 

4. OOB 임의 주소 읽기

4-1. 실습 전, 준비

  • 실습 코드
// Name: oob_read.c
// Compile: gcc -o oob_read oob_read.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char secret[256];

int read_secret() {
  FILE *fp;

  if ((fp = fopen("secret.txt", "r")) == NULL) {
    fprintf(stderr, "`secret.exe` does not exist");
    return -1;
  }
  fgets(secret, sizeof(secret), fp);
  fclose(fp);

  return 0;
}


int main() {
  char *docs[] = {"COMPANY INFORMATION", "MEMBER LIST", "MEMBER SALARY",
                  "COMMUNITY"};
  char *secret_code = secret;
  int idx;

  // Read the secret file
  if (read_secret() != 0) {
    exit(-1);
  }

  // Exploit OOB to print the secret
  puts("What do you want to read?");
  for (int i = 0; i < 4; i++) {
    printf("%d. %s\n", i + 1, docs[i]);
  }

  printf("> ");
  scanf("%d", &idx);
  
  if (idx > 4) {
    printf("Detect out-of-bounds");
    exit(-1);
  }

  puts(docs[idx - 1]);
  return 0;
}

 

  • secret.txt 파일을 만들어준다.
$ echo "THIS IS SECRET" > ./secret.txt

 

 

4-2. 실습 과정 & 결과

 

(1) 임의 주소 값을 구하려면?

  • OOB로 임의 주소 값을 읽으려면, 읽으려는 변수와 배열의 오프셋을 알아야 한다.
  • 배열과 변수가 같은 세그먼트에 할당되었다면 둘 사이의 오프셋은 항상 일정하므로 디버깅을 통해 쉽게 알 수 있지만,
    같은 세그먼트가 아니라면 다른 취약점을 통해 두 변수의 주소를 구하고 차이를 계산해야 한다.

(2) 실습 코드 분석

  • 인덱스에 대한 검증이 미흡해, 임의 주소 읽기가 가능한 코드이다.
  • 배열의 크기가 3인 docs를 참조하는데 인덱스 값이 3보다 큰 것은 검사하지만, 음수는 검사하지 않는다.

(3) 실습 과정 및 결과

  • docs와 secret_code는 지역변수로 스택에 할당되어 있다. 
    이에 같은 세그먼트에 할당되어 있기 때문에, docs에 대한 OOB를 통해 secret_code값을 읽을 수 있다.
  • 입력 값에 0을 입력하면 docs[0 - 1]이 되어서, docs[-1]이 된다. 이로 인해, OOB가 발생하고 secret.txt 파일의 값을 읽을 수 있다.

5. OOB 임의 주소 쓰기

5-1. 실습 코드

// Name: oob_write.c
// Compile: gcc -o oob_write oob_write.c

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

struct Student {
  long attending;
  char *name;
  long age;
};

struct Student stu[10];
int isAdmin;

int main() {
  unsigned int idx;
  // Exploit OOB to read the secret
  
  puts("Who is present?");
  printf("(1-10)> ");
  scanf("%u", &idx);
  
  stu[idx - 1].attending = 1;
  
  if (isAdmin) 
  	printf("Access granted.\n");
    
  return 0;
}

 

 

5-2. 실습 과정 & 결과

 

(1) 실습 코드 분석

  • 인덱스에 대한 검증이 미흡해, 임의 주소에 값을 쓸 수 있는 코드이다.
  • 24byte 크기의 Student 구조체가 존재하며, 해당 구조체 10개를 포함하는 배열 stu와 isAdmin이 전역 변수로 선언되어 있다.
  • 인덱스를 입력받아와, 인덱스에 해당하는 Student구조체의 attending에 1을 대입한다.
  • isAdmin이 True이면 "Access granted."가 출력된다.

 

(2) 실습 과정 및 결과

  • stu와 isAdmin 둘 다 전역변수로 같은 data세그먼트에 할당되어 있다.
  • 디버거로 주소를 비교해보면, isAdmin이 stu보다 240바이트가 높은 주소에 있는 것을 확인할 수 있다.
    ( 0x201130 - 0x201040 = 0xF0 )

 

  • Student구조체의 크기가 24byte로 10번째 인덱스를 참조하면, isAdmin에 1이 작성되도록 조작할 수 있다.

6. 해결방안

  • 프로그램에서 배열을 사용할 때 인덱스의 값이 음수인지, 배열의 크기가 넘어서지 않는지 검사하는 코드가 포함되어야 한다.