[Pwnable] 13. Memory Corruption: Out of Bounds

Wonder_Land🛕·2022년 11월 4일
0

[Pwnable]

목록 보기
13/21
post-thumbnail

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.


  1. 서론
  2. Out of Bounds
  3. Q&A
  4. 마치며

1. 서론

프로그램을 개발할 때 같은 자료형의 변수나 객체를 여러 개 관리해야 하면,
이들을 요소로 하는 배열을 선언하여 사용합니다.

배열은 같은 자료형의 '요소(element)'들로 이루어져 있는데,
각 요소의 위치를 '인덱스(index)'라고 합니다.

우리가 처음 C언어를 배우고 배열을 사용할 때,
배열의 인덱스와 관련하여 자주 실수를 합니다.

현실에서는 첫 번째 요소의 인덱스를 '0'으로 지정해야하는데서 발생하는 인지적 실수, 부등호와 관련된 실수, 인덱스를 넘어서 참조하는 등의 경고를 띄워주지 않는 컴파일러 등이 있을 수 있습니다.

운이 좋다면, 프로그램이 비정상적으로 종료되지만,
그렇지 않다면 취약점의 원인이 될 수도 있습니다.

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

사실 'Out of Bounds'라는 용어는 농구와 같은 스포츠에서 필드나 코스를 벗어난 행위를 일컫습니다.


2. Out of Bounds

1) 배열의 속성

배열은 연속된 메모리 공간을 점유하며,
배열이 점유하는 공간의 크기는 '(요소의 개수) * (요소 자료형의 크기)'가 됩니다.

배열의 각 요소의 주소는 '(배열의 주소) + (요소의 인덱스) * (요소 자료형의 크기)'를 이용하여 계산됩니다.

2) Out of Bounds

OOB는 요소를 참조할 때, 인덱스의 값이 음수이거나 배열의 길이를 벗어날 때 발생합니다.

개발자가 인덱스의 범위에 대한 검사를 명시적으로 프로그래밍하지 않는다면,
프로세스는 앞서 배운 식을 따라 요소의 주소를 계산할 뿐, 계산한 주소가 배열의 범위 안에 있는지 검사하지 않습니다.

따라서 만약 사용자가 배열 참조에 사용되는 인덱스를 임의 값으로 설정할 수 있다면,
배열의 주소로부터 특정 오프셋에 있는 메모리 값을 참조할 수 있습니다.

이를 배열의 범위를 벗어나는 참조라 하여 'Out of Bounds'라고 합니다.

(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;
}
$ ./oob
In Bound:
arr: 0x7ffebc778b00
arr[0]: 0x7ffebc778b00

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

위의 코드는 int형 변수 10개를 요소로 하는 배열 arr을 선언하고, 다양한 인덱스를 사용하여 배열 내부와 외부의 주소를 출력합니다.

결과를 보면 이상한 점이 있습니다.

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

  2. arr[0](0x7ffebc778b00)arr[100](0x7ffebc778c90)의 주소 차이가 0x7ffebc778c90 - 0x7ffebc778b00 = 0x190 = 100 * 4(int의 크기)입니다.
    배열의 범위를 벗어난 인덱스를 참조해도 앞서 살펴본 식을 그대로 사용함을 알 수 있습니다.

(2) 임의 주소 읽기

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보다 큰지만 검사하고, 음수인지는 검사하지 않습니다.

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

secret.txt 파일을 만들고, oob_read의 OOB를 이용하여 해당 파일의 값을 읽어보겠습니다.

$ 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

(3) 임의 주소 쓰기

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개를 포함하는 배열 stuisAdmin를 전역 변수로 선언합니다.

그리고 사용자로부터 인덱스를 입력받아서 인덱스에 해당하는 Student 구조체의 attending1을 대입합니다.

예제 코드의 마지막 부분을 보면 isAdmin이 True인지 False인지 검사하는 부분이 있습니다.

해당 변수에 값을 직접 쓰는 부분은 없지만,
코드에 OOB 취약점이 있으므로 이를 이용하여 isAdmin의 값을 조작할 수 있습니다.

stuisAdmin의 주소를 확인해보면,
isAdminstu보다 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_write
Who is present?
(1-10)> 11
Access granted.

3. Q&A

-


4. 마치며

gcc는 배열의 인덱스와 관련한 취약점 Out of Bounds 취약점에 대해 아무런 경고도 띄워주지 않습니다.

따라서 배열을 사용할 때는 인덱스 값이 음수인지, 길이를 넘어가지 않는지 검사하는 습관을 길러야 합니다.

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.

profile
아무것도 모르는 컴공 학생의 Wonder_Land

0개의 댓글