Buffer Overflow 에 대해 알아보자
최근에 발표되고 있는 보안 버그의 상상수가 overflow를 이용한 버그이다.
그만큼 overflow버그는 적용되는 프로그램의 수가 많다는 뜻이다.
overflow에 대한 의식없이 작성된 대부분의 프로그램이 이 버그를 가지고
있다고 생각해도 될 것이다.
그럼.. overflow버그에 대해 이해하고 그에 대한 해결책을 생각해 보자.
Overflow란 무엇인가?
overflow란 말그대로 넘친다는 뜻이다. 그렇다면 무엇이 넘친다는 뜻인가?
일반적인 overflow버그는 buffer가 넘친다는 뜻으로 이해하면 될것이다.
프로그래밍 당시 정해준 버퍼의 크기 이상의 데이타를 버퍼에 저장한다면
말그대로 넘치게 될것이다.
이렇게 넘친 데이타는 프로그램의 다음 수행에 신뢰성을 제공하지 못하게 된다.
buffer overflow 버그를 이용한 해킹은 바로 이러한 buffer를 임의로 overflow
시켜서 자신이 원하는 일을 수행하도록 하는것이다.
아래의 내용을 계속 읽어 나가면서 그 원리를 이해해 보자.
프로세스 메모리 구조
실제로 프로그램이 메모리 상에서 수행될때의 프로세스 메모리 구조를 보자.
+-------------------+
| Text | Lower Memory Address
+-------------------+
| Data |
+-------------------+
| Stack | Higher Memory Address
+-------------------+
위의 그림에서 Text 부분은 프로그램이 들어가는 자리이다.
Data부분에는 이미 설정되어 있는 data들, static 변수들 등의 데이타 영역이다.
그리고, 이번 강좌에서 가장 중요한 stack 영역이다. 이 stack 영역에 대해서
아래에서 자세히다루어 보자.
FONT SIZE=4>스택영역
스택이란것이 LIFO 구조의 기억장소라는 것을 모르는 사람은 별로 없을 것이다.
여기서의 스택 영역은 임시로 기억되어야 할 내용을 담아둔다. sub 함수가
호출될때의 주소, 지역변수 등등이다.
stack 영역은 Data 영역과 반대로 Higher Memory Address에서 Lower Memory Address로
커져나간다. 스택의 운영이 어떻게 이루어 지는지 보자.
프로세스 메모리 구조의 stack 은 sub function이 호출되면 하나씩 PUSH 되고
sub function의 수행이 종료되고 return 되면 POP 이 된다.
그럼 어떠한 정보가 PUSH되고 POP되는 것인가??
가장 중요한 정보는 다음 수행 위치를 가리키는 PC(Program Counter)값이다.
다음 예를 보자.
subfunc(char *lbuf)
{
char sbuf[10];
strcpy(sbuf,lbuf);
}
main()
{
char lbuf[20];
subfunc(lbuf);
}
위의 경우에 main()에서 subfunc를 호출할 경우에 stack은 다음과 같은 형태를 가지게 된다.
+-------------------+----------------------+
lower | subfunc() | main() | higher memory address
+-------------------+----------------------+
C 프로그램에서 실제로 main()함수는 start()라는 함수에 의해 최초로 호출이 된다.
그때 위와 같은 main()의 stack영역이 PUSH되어 만들어 지게 되고 위의 프로램에서
subfunc()가 호출이 되면 subfunc() stack 영역이 PUSH되어 결국 우와 같은 형태를
가지게 된다. 위에서 얘기했듯이 stack영역은 하위 메모리 영역으로 커져 나간다.
그럼.. 여기서 각 stack 의 element 의 내용은 어떠한 것이 있는지 알아보고
실제 overflow에 대해 알아보자.
+--------+-------+---+---+---+------------+---+-------+-------+
sp 32 24 4 4 4 24+8*x 4 8*y 16 fp
gotargs sfp ret putargs l-v
위의 그림은 stack memory map이다. 함수가 호출되면 최소 112 바이트를 스택영역으로
잡게 되고 그 내용은 위와 같이 되는데, 위의 x, y값에 의해 스택 영역은 112바이트를
넘어갈 수도 있다.
각 내용을 보면 두 번째 24바이트의 영역은 getargs로 이전의 function으로부터 넘어온
argument들을 담아둔다. 최대 6개까지 담을 수 있다. 다음의 4바이트는 sfp값으로 이전
function의 sp값을 가지며, 다음의 4바이트가 ret로 이전 function에서 call되었던
주소를 가지고 있다가 함수 수행이 끝나면 이 값으로 원래 함수로 돌아가게 된다.
위에서 말한 PC값이다. 바로 이 ret가 stack overflow에서 한 몫을 하는 중요한 요소이다.
그다음 24+8*x는 다른 함수에 전해줄 argument를 담는 부분이고, 8*y는 이 함수에 선언된
지역 변수들을 저장하는 장소이다.
여기서 설명안된 부분은 대현이도 잘 모르지요...
위에서 sp는 스택의 top이라 생각하면 될 것이다. fp(frame pointer)는 한 개의 function
스택 element의 시작이라 하는게 옳겠다.
이제부터 어떻게 overflow를 이용한 해킹이 가능한지 알아보겠다.
FONT SIZE=4>overflow를 이용한 해킹
여기서 위의 소스 코드를 다시 언급해 보겠다.
subfunc(char *lbuf)
{
char sbuf[100];
strcpy(sbuf,lbuf);
}
main()
{
char lbuf[200];
subfunc(lbuf);
}
다시 위의 코드로 스택영역을 그려보자.
sp of subfunc fp of subfunc
sp of main fp of main
+--------------------------+------------------------------+
| [ret] [sbuf] | [ret] [lbuf] |
+--------------------------+------------------------------+
위에서 보다 좀더 구체적으로 들어간다.
위의 [sbuf]와 [lbuf]는 위의 스택 구성요소중 8*y에 해당하는 자동 변수부분이다.
우리는 여기서 위의 프로그램의 문제점을 발견해야 한다.
subfunc()함수의 strcpy(sbuf,lbuf); 라는 내용이다. 이부분이 수행되었을 때
일어날 일에 대해 생각해 보자.
만약 lbuf에 'a'라는 값이 전체에 저장되어 있다고 가정해 보자. 이때 카피후의
스택을 그려보면 다음과 같이 될 것이다.
sp of subfunc fp of subfunc
sp of main fp of main
+--------------------------+------------------------------+
| [ret] [sbuf] | [ret] [lbuf] |
| aaaaaaaaaaaaaaaaaaaaaaaaaaa...
+--------------------------+------------------------------+
sbuf의 크기는 100바이트이고 그 뒤의 16바이트, 그리고 main 스택영역의 앞부분
84바이트를 'a'로 채우게 된다.
뭔가 떠오르는 것이 있을 것이다. main()의 [ret]값이 'aaaa'로 바뀐 것이다.
그럼 어떤 일이 벌어질까? main()함수의 수행이 끝나고 return 하려는 순간
0x61616161('aaaa')로 가서 다음 수행을 계속하려 하는 일이 발생한다.
이때 Segmentation Fault가 발생하게 된다.
이제, 여기서 우리는 한가지를 더 깨달아야 한다.
위의 'a' 대신에 다른값을 넣어서 다음 수행위치를 내가 원하는 곳으로 바꿀수 있다는 것이다.
자 다음을 보자.
위의 aaa라는 값 대신에 다음과 같은 값을 넣어보자.
sp of subfunc fp of subfunc
sp of main fp of main
+--------------------------+------------------------------+
| [ret] [sbuf] | [ret] [lbuf] |
| NNNNNNNSSSSSSSSRRRRRRRRRRRRR... |
+--------------------------+------------------------------+
위에서 N(NOP: NO Operation)은 아무일도 하지 않는 기계어 코드를
S(Shell code)는 루트쉘을 수행하는 코드를
R(Return address)는 위의 N이 저장된 부분의 주소값을 의미한다.
위와 같은 값을 저장한다면 main()의 ret에 저장된 주소는 N의 부분을
나타내고 있으므로 main 수행후 다음 PC는 N이 있는 위치로 이동하게 되고
순서대로 수행하게 된다. 이때 N은 NOP이므로 아무런 변화없이 계속 수행하다,
S부분에 도착하면 shell이 수행되고 shell이 뜨게 될 것이다. 여기서 N을 사용한
이유는 실제로 정확한 buffer size를 알수 없고 return address또한 정확히 Shell code
부분에 맞추기가 힘들기 때문에 약간의 여유를 주는 것이다.
결론이 너무 간단하게 난 듯한 느낌이 든다.
실제로 오버플로우를 이용한 버그는 알고 보면 그리 어려운 내용도 아니다.
아래에 실제 예를 가지고 더 자세히 알아보자.
해결책
이 버그의 해결책은 가장좋은 방법은 업체로부터 패치를 얻거나, 또는 새로운
버전을 설치하는 것이다.
참고로, 위와같이 argument 로 넘어오는 data로서 buffer를 overflow시켜 공격하는
방법에 대한 임시 패치가 가능하다.
실제 프로그램은 다른 이름으로 바꾸고, 새로운 wrapper 프로그램을 만들어서
argument의 size가 정해준 size를 초과하지 않을 경우에만 실제 프로그램을
호출하여 주는 프로그램을 만들어 주면 될 것이다. AUSCERT에서 발표한 overflow_wrapper.c 라는 것이 있다.
이 글을 읽는 여러분들은 이러한 일이 일어나지 않도록 안전한 프로그래밍 습관을 길러야 할 것이다.
Boundary를 점검하는 함수는 fgets(), strncpy(), strncat() 등이 있다. 이런 함수들을 애용하기 바란다.
참고로 Boundary를 점검하지 않는 함수들은 strcat(), strcpy(), gets(), sprintf(), sscanf() 등이 있다.

Comments List