Out of Bounds
배열의 속성
배열은 연속된 메모리 공간을 점유하며, 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료형의 크기를 곱한 값이된다. 배열이 포함하는 요소의 개수를 배열의 길이라고 부른다.
배열 각 요소의 주소는 배열의 주소, 요소의 인덱스, 요소 자료형의 크기를 이용하여 계산된다.
Out of Bounds
OOB는 요소를 참조할 때, 인덱스 값이 음수이거나 배열의 길이를 벗어날 때 발생한다. 개발자가 인덱스의 범위에 대한 검사를 명시적으로 프로그래밍하지 않으면, 프로세스는 앞서 본 식을 따라 요소의 주소를 계산할 뿐, 계산한 주소가 배열의 범위 안에 있는지 검사하지 않는다.
만약 사용자가 배열 참조에 사용되는 인덱스를 임의 값으로 설정할 수 있다면, 배열의 주소로부터 특정 오프셋에 있는 메모리의 값을 참조할 수 있다. 이를 배열의 범위를 벗어나는 참조라 하여 Out of Bounds라고 한다.
// 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을 선언하고, 다양한 인덱스를 사용하여 배열 내부와 외부의 주소들을 출력한다.
결과를 보면 -1과 100을 인덱스로 사용했음에도 아무런 경고를 띄워주지 않는다. OOB를 방지하는 것은 전적으로 개발자의 몫이다.
arr[0]와 arr[100]의 주소 차이는 0x7ffc2fd28a70 - 0x7ffc2fd288e0 = 0x190 = 100 * 4이다. 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);
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;
}
OOB로 임의 주소의 값을 읽으려면, 읽으려는 변수와 배열의 오프셋을 알아야한다. 배열의 변수가 같은 세그먼트에 할당되어 있다면, 둘 사이의 오프셋은 항상 일정하므로 디버깅을 통해 쉽게 알아낼 수 있다. 만약 같은 세그먼트가 아니라면, 다른 취약점을 통해 두 변수의 주소르 구하고, 차이를 계산해야 한다.
위 예제 코드를 보면 길이가 3인 배열 docs를 참조하는데, 인덱스 값이 3보다 크지만 음수인지는 검사하지 않는다. docs와 secret_code는 모두 스택에 할당되어 있으므로, docs에 대한 OOB를 이용하면 secret_code의 값을 쉽게 읽을 수 있다.
임의 주소 쓰기
// 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바이트이고 10개를 포함하는 배열 stu와 isAdmin를 전역 변수로 선언한다. 그리고 사용자로부터 인덱스를 입력받아 구조체의 attending에 1을 대입한다. OOB 취약점을 이용해 isAdmin의 값을 조작하면 "Access granted"를 출력할 수 있다.
stu와 isAdmin의 거리는 0xf0(240)바이트가 차이가 난다. 이를 주의하고 OOB를 이용해 조건문을 통과해보자.
구조체의 크기가 24바이트이고 10개의 배열이 존재하니 24 * 10 = 240 attending에 1을 대입할때 idx - 1을하니 11을 입력을하면 조건을 통과한다.