공부중/시스템 해킹

[Dreamhack] Memory Corruption: Out of Bounds

silver surfer 2024. 9. 28.

프로그램을 개발할 때 같은 자료형의 변수나 객체를 여러 개 관리해야 하면 이들을 요소로 하는 배열을 선언하여 사용한다. 배열은 같은 자료형의 요소(Elements)들로 이루어져 있는데, 각 요소의 위치를 인덱스(Index) 라고 한다.

처음 C언어에서 배열을 배우고, 사용할 때, 배열의 인덱스와 관련된 부분에서 자주 실수를 한다. 현실에서는 첫 번째 요소라고 할 것을 프로그래밍 할 때는 0번째 요소라고 해야하는데서 발생하는 인지적 실수, 사소한 부등호 실수, 그리고 인덱스를 벗어나서 참조할 수 있어도 경고를 띄워주지 않는 컴파일러 등이 주요 원인일 것이다.

위와 같은 실수는 운이 좋으면 프로그램의 비정상 종료로 그치지만, 때에 따라 치명적인 취약점의 원인이 될 수있다.

대표적으로는 배열의 임의 인덱스에 접근할 수 있는 Out of Bounder(OOB)가 있다.

이번 강의에서는 OOB 취약점이 발생하는 코드의 유형과 OOB를 공격해서 얻을 수 있는 효과에 대해 살펴볼 것이다.

💡Out of Bounds의 유래

Out of Bounds는 농구와 같은 스포츠에서 필드, 강의를 벗어난 행위를 일컫습니다. 이 취약점도 배열의 경계를 넘어서서 값을 읽거나 쓸 수 있다는 점이 이와 유사하여 Out of Bounds라고 이름 붙여졌습니다.

 

** Out of Bounds

배열의 속성

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

배열의 크기

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

배열의 참조

 

Out of Bounds

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

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

Out of Bounds

 

Proof-of-Concept

다음 코드는 OOB의 이해를 돕기 위한 예제 코드이다. 예제는 int형 변수 10개를 요소로 하는 배열 arr을 선언하고, 다양한 인덱스를 사용하여 배열 내부와 외부의 주소들을 출력한다.

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

 

예제를 컴파일하고 실행하면, 다음과 같은 결과를 확인할 수 있다.

$ gcc -o oob oob.c
$ ./oob
In Bound:
arr: 0x7ffebc778b00
arr[0]: 0x7ffebc778b00

Out of Bounds:
arr[-1]: 0x7ffebc778afc
arr[100]: 0x7ffebc778c90

결과에서 주목해야할 것은 다음과 같다.

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

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

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

 

임의 주소 읽기

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

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

// 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.txt` 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;
}

docs와 secret_code은 모두 스택에 할당되어 있으므로, docs에 대한 OOB를 이용하면 secret_code의 값을 쉽게 읽을 수 있다. secret.txt 파일을 만들고, oob_read의 OOB를 이용하여 secret.txt 값을 읽어보자.

 

$ echo "THIS IS SECRET" > ./secret.txt
$ ./oob_read
What do you want to read?
1. COMPANY INFORMATION
2. MEMBER LIST
3. MEMBER SALARY
4. COMMUNITY
> 0
THIS IS SECRET

 

임의 주소 쓰기

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

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

 

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

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

pwndbg> i var isAdmin
Non-debugging symbols:
0x0000000000201130  isAdmin
pwndbg> i var stu
Non-debugging symbols:
0x0000000000201040  stu
pwndbg> print 0x201130-0x201040
$1 = 240

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

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

 

$ ./oob_write
Who is present?
(1-10)> 11
Access granted.

마치며

마치며

이번 강의에서는 배열의 범위를 벗어난 영역을 참조하는 out of bounds 취약점에 대해 알아보았습니다. 앞서 살펴봤듯, 이 취약점과 관련해서는 gcc에서 아무런 경고도 띄워주지 않습니다. 그래서 실수로 인덱스를 검사하는 코드를 생략하면, 이를 나중에 발견하기는 매우 어렵습니다. 그러므로 코드를 작성할 때, 인덱스 값이 음수인지, 배열의 길이를 넘어서지는 않는지 검사하는 습관을 들이는 것이 바람직합니다.

다음 강의에서는 포맷 스트링을 사용하는 printf, fprintf, sprintf, snprintf 함수 등에서 발생할 수 있는 Format String Bug에 대해 배워보겠습니다. 🚩

키워드
  • 배열의 길이: 배열이 포함하는 요소의 개수
  • 배열의 크기: 배열의 길이 X 요소의 크기
  • 배열의 참조: 배열의 주소, 요소의 크기, 인덱스를 활용하여 참조할 요소의 주소를 계산함. 이 과정에서, 계산된 주소가 배열의 범위를 벗어나는지 검사하지 않으므로 out of bounds 취약점이 발생할 수 있음.
  • Out of Bounds(OOB): 배열의 범위를 벗어난 메모리에 접근할 수 있는 취약점. 개발자가 인덱스에 대한 검사를 제대로 하지 않으면 발생함. 임의 주소 읽기, 임의 주소 쓰기로 이어질 수 있음.

 

Q1. OOB 취약점을 방어하기 위해 [A] 위치에 들어갈 올바른 검증 코드는?

#include <stdio.h>
int main() {
  int buf[0x10];
  unsigned int index;
  
  scanf("%d", &index);
  [A]
  printf("%d\n", buf[index]);
  return 0;
}

if (index >=  0x10) {exit(-1);}

 

 

 

 

 

 

 

 

댓글