출처 드림핵
배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료형의 크기를 곱한 값이 된다. 요소의 개수를 우리는 배열의 길이라고 부른다. 배열의 각 주소는 배열의 주소, 요소의 인덱스, 요소 자료형의 크기를 이용하여 계산된다.
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;
}
OOB를 이용해 임의 주소의 값을 읽으려면, 읽으려는 변수와 배열의 오프셋을 알아야한다. 배열과 변수가 같은 세그먼트에 할당되어 있다면, 둘 사이의 오프셋은 항상 일정하므로 디버깅을 통해 쉽게 알아낼 수 있다. 만약 그렇지 않다면, 다른 취약점을 통해 두 변수의 주소를 구하고, 차이를 계산해야 한다.
문제 출처
$ checksec out_of_bound
[*]
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
ASLR, NX, Canary가 적용되어 있다.
소스코드는 다음과 같다.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
char name[16];
char *command[10] = { "cat",
"ls",
"id",
"ps",
"file ./oob" };
void alarm_handler()
{
puts("TIME OUT");
exit(-1);
}
void initialize()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main()
{
int idx;
initialize();
printf("Admin name: ");
read(0, name, sizeof(name));
printf("What do you want?: ");
scanf("%d", &idx);
system(command[idx]);
return 0;
}
idx 범위를 검사하지 않기 때문에, OOB를 활용할 수 있다. command와 name 전역변수는 PIE가 꺼져있기 때문에, 실행시마다 일정한 주소에 위치한다. 따라서 command 배열에 적절한 인덱스를 집어 넣으면 name에 접근할 수 있다.
따라서 name에 "/bin/sh\x00"을 저장하고 적절한 인덱스로 이를 가리켜 보자. 참고로 system 함수는 eax 레지스터의 값을 받는데, 이때 레지스터에는 "/bin/sh\x00"을 가리키는 주소 가 들어가야 한다.
따라서 페이로드 구성은 | /bin/sh\x00<8byte>| /bin/sh\x00을 가리키는 주소<4byte> | 다.