[ Out of bounds ]

Out of bounds

▶ 배열의 속성

  배열은 연속된 메모리 공간을 점유하며, 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료형의 크기를 곱한 값이 된다. 흔히, 배열이 포함하는 요소의 개수를 배열의 길이 (Length)라고도 부른다.

배열의 크기

 

배열 각 요소의 주소는 배열의 주소, 요소의 인덱스 요소 자료형의 크기를 이용하여 계산된다.

배열의 속성

 

▶ Out of Bounds

  OOB는 요소를 참조할 때, 인덱스 값이 음수이거나 배열의 길이를 벗어날 때 발생한다. 개발자가 인덱스의 범위에 대한 검사를 명시적으로 프로그래밍하지 않으면, 프로세스는 앞서 배운 식을 따라 요소의 주소를 계산할 뿐, 계산한 주소가 배열의 범위 안에 있는지 검사하지 않는다.

  따라서 만약 사용자가 배열 참조에 사용되는 인덱스를 임의 값으로 설정할 수 있다면, 배열의 주소로부터 특정 오프셋에 있는 메모리의 값을 참조할 수 있다. 이를 배열의 범위를 벗어나는 참조라 하여 Out of Bounds라고 부른다.

 

▶ Proof-of-Concept

// 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;
}

  int형 변수 10개를 요소로 하는 배열 arr을 선언하고, 다양한 인덱스를 사용하여 배열 내부와 외부의 주소들을 출력한다. 이를 컴파일하고 실행하면 아래와 같은 결과를 얻을 수 있다.

  먼저, 컴파일러(gcc)는 배열의 범위를 명백히 벗어나는 -1과 100을 인덱스로 사용했음에도 아무런 경고를 띄워주지 않는다. 즉, OOB를 방지하는 것은 전적으로 개발자의 몫이다.

  다음으로, arr[0]와 arr[100]의 주소 차이가 0x7ffdea2c05d0−0x7ffdea2c0440=0x190=100×4이다. 배열의 범위를 벗어난 인덱스를 참조해도 앞서 살펴본 식을 그대로 사용함을 확인할 수 있다.

  OOB가 실제로 가능함을 확인해 보았으니, 이제는 OOB를 이용한 임의 주소 읽기와 임의 주소 쓰기에 대해 살펴보자.

 

▶ 임의 주소 읽기

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

// 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;
}

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

  docs와 secret_code은 모두 스택에 할당되어 있으므로, docs에 대한 OOB를 이용하면 secret_code의 값을 쉽게 읽을 수 있다.

  secret.txt파일을 만들고, oob_read의 OOB를 이용하여 secret.txt의 값을 읽어보자.

 

▶ 임의 주소 쓰기

  OOB를 이용하면 임의 주소에 값을 쓰는 것도 가능하다.

// 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;
}

  위 코드는 인덱스에 대한 검증이 미흡해 임의 주소에 값을 쓸 수 있는 예제이다. 코드를 살펴보면, 24바이트 크기의 Student 구조체 10개를 포함하는 배열 stu와 isAdmin를 전역 변수로 선언한다. 그리고 사용자로부터 인덱스를 입력받아서 인덱스에 해당하는 Student구조체의 attending에 1을 대입한다.

  예제 코드의 마지막 부분을 보면 isAdmin이 참인지 검사하는 부분이 있다. 해당 변수에 값을 직접 쓰는 부분은 없지만, 코드에 OOB취약점이 있으므로 이를 이용하여 isAdmin의 값을 조작할 수 있다.

  이를 위해 디버거로 stu와 isAdmin의 주소를 확인해보면, isAdmin이 stu보다 240바이트 높은 주소에 있음을 알 수 있다.

 

  배열을 구성하는 Student 구조체의 크기가 24바이트이므로, 10번째 인덱스를 참조하면 isAdmin을 조작할 수 있다.

다음과 같이, 예제를 컴파일하고 OOB취약점을 공격하여 isAdmin값을 조작해보자.

+ Recent posts