보통 프로그래밍 실수에 의해 발생하고
OS와 애플리케이션의 취약점을 이용할 수 있다.
보통 레거시코드, 업데이트 실패 등도 공격이 발생하는 데 한 몫한다.
모리스 웜이 처음으로 fingered를 버퍼오버플로우에 사용했다.
1995, ncsa 서버에서 버퍼오버플로우 취약점이 발견되었다.
버퍼오버플로우 취약점을 이용하는 공격방법을 차례차례 기술해서 발간.
2001, Red worm이 Microsoft IIS 5.0의 버퍼오버플로우 취약점을 이용함,
등이 있다.
일단 버퍼 오버플로우랑 같은 뜻의 말이 buffer overrun
, buffer overwrite
등이 있다.
다음은 간단한 버퍼 오버플로우 취약점을 가진 코드이다.
// buffer1.c
int main(int argc, char *argv[]) {
int valid = FALSE;
char str1[8];
char str2[8];
next_tag(str1); // next_tag는 str1에 "START" 를 넣어주는 일을 한다.
gets(str2); // get user input
if (strncmp(str1, str2, 8) == 0)
valid = TRUE;
printf("buffer1: str(%s), str2(%s), valie(%d)\n", str1, str2, valid);
}
$ cc -g -o buffer1 buffer1.c
$ ./buffer1
START
buffer1: str1(START), str2(START), valid(1)
$ ./buffer1
EVILINPUTVALUE
buffer1: str1(TVALUE), str2(EVILINPUTVALUE), valid(0)
$ ./buffer1
BADINPUTBADINPUT
buffer1: str(BADINPUT), str2(BADINPUTBADINPUT), valid(1)
다음을 알고 있어야한다.
Local variables and arrays are usually saved in consecutive memory locations 👉🏻 str1
과 str2
가 local variable임을 잊지말자.
C strings are 'NULL-terminated'
gets()
doesn't check the buffer size
str2
는str1
을 오버플로우하게 되고 두번째 세번째와 같은 결과가 나오게 되는 것이다. 그림으로 다시 살펴보자.
사용자 Input을 받기 전의 스택 상태이다.
그림을 보면 str1 자리에 "START"가 있음을 알수 있다. str2에 이제 입력 "BADINPUTBADINPUT"이 들어왔다고 치자.
str2에 할당된 사이즈는 8이라서 8칸 이후부터는 str1이 할당받은 공간인데, 입력을 받는 함수인 gets()가 버퍼의 크기를 확인하지 않고 저장해버리기 때문에 8글자가 넘는 순간 str1의 공간을 overwrite하게 되는 것이다.
INPUT이 앞의 8글자와 뒤의 8글자가 같으므로 str1과 str2의 앞의 8글자만 비교하는 strncmp()
의 반환 값은 당연히 0이 되고 따라서 valid값이 1(TRUE
)이 되는 것이다. 비교함수는 정상적인 결과를 내놓는 것이다 😥
그런데 만약 입력의 길이가 너무 길어서 str1을 넘어서까지 overwrite한다면 어떤 일이 일어날까??
그런. 경우를 잘 보여주는 프로그램이 있다! 다음 코드를 한번 보자.
// buffer2.c
void hello(char *tag)
{
char inp[16];
printf("Enter value for %s: ", tag); gets(inp);
printf("Hello your %s is %s\n", tag, inp);
}
$ cc -g -o buffer2 buffer2.c
$ ./buffer2
Enter value for name: Bill and Lawrie
Hello your name is Bill and Lawrie
buffrer done
$ ./buffer2
Enter value for name: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Segmentation fault (core dumped)
일단 segmentation fault라 하믄.... 내가 c로 다이어리 만들 때 골머리 앓던 주 원인이던 애인데 얘를 여기서 보다니..
일단 세그폴트가 뭔지 찾아보면, 우리의 위키백과는 다음과 같이 정의하고 있다.
프로그램이 허용되지 않은 메모리 영역에 접근을 시도하거나, 허용되지 않은 방법으로 메모리 영역에 접근을 시도할 경우 발생한다. - wikipedia
이 프로그램의 경우에는,
Long input이 저장돼있던 frame pointer와 return address까지 overwrite해버려서 쓰레기 값이 들어가게 되고, 프로그램이 해당 쓰레기 값을 주소로 읽어서 이상한 곳에 접근하면 발생하는 에러인 것이다.
👉🏻 어?? 그럼 return address를 내가 원하는 address로 overwrite할 수도 있겠네?
👉🏻 Yes ❣️
return address를 원하는 주소로 덮어쓰기 하는 케이스를 살펴보자.
$ perl -e 'print pack("H*", "414243444546474851525354555657586162636465666768e8ffffbf948304080a4e4e4e4e0a");' | ./buffer2
Enter value for name:
Hello your Re?pyy]uEA is ABCDEFGHQRSTUVWXabcdefguyu
Enter value for Kyyu:
Hello your Kyyu is NNNN
Segmentation fault (core dumped)
펄스크립트의 pack()
을 써서 출력값을 파이프라인으로 ./buffer2 프로그램에 넘겨주면 unreadable charactrer인 주소값까지 넣어줄 수 있다. 아스키코드를 보면 대충 알 수 있다.
펄스크립트에서 pack을 해주기 전의 문자열은 hex값이고, 414243444546474851525354555657586162636465666768
를 char로 변환하면 이전에 썼었던 ABCDEFGHQRSTUVWXabcdefguyu
와 같다는 걸 알 수 있다. 뒤에 있는 애들 때문에 hex로 표현을 한 거다.
그럼 다시 한 번 보자. 일단 우리가 입력한 hex값이 메모리에 어떻게 들어가는지 그림으로 보는게 좋겠다.
이런식으로 메모리에 hex값이 저장되고 char로 변환될 수 있는 애들은 밑에 변환이 되어있다.
이때 return addr에 0x08048394
가 들어간걸 확인할 수 있다. 이게 바로 hello()의 주소이다. 그래서 우리가 입력을 했을 때 hello가 두번 호출이 된 거다!
두번째 hello()실행결과는 다음 글에서 다뤄보도록 하자...
다음글에서 고고 ☄️