강좌
클라우드/리눅스에 관한 강좌입니다.
해킹&보안 분류

리눅스마스터1급: 버퍼 오버플로(Buffer overflow) 공격

작성자 정보

  • 관리자 작성
  • 작성일

컨텐츠 정보

본문

리눅스마스터1: 버퍼 오버플로(Buffer overflow) 공격

 

 

 

 

버퍼 오버플로(Buffer Overflow) 공격이라는 말은 보안에 관심이 있는 사람이라면 누구나 한 번쯤은 들어본 말일 것이다.

 

 

 

버퍼 오버플로 공격이란 말 그대로 지정된 버퍼보다 더 많은 데이터를 입력해서 프로그램이 비정상적으로 동작하도록 만드는 것을 의미한다.

 

 

 

C 언어에서는 데이터가 지정된 버퍼보다 더 많이 입력되었는지를 체크하지 않고 메모리에 쓰는 것이 일반적이다.

 

 

 

이것은 하나하나 체크를 하다보면 프로그램의 수행 성능이 크게 저하되기 때문이다.

 

 

 

이러한 사실은 옛날부터 알려져 왔지만 오랫동안 특별히 많은 사람들의 관심을 끌지 못했다.

 

 

 

프로그램이 비정상적으로 종료되는 것 이외에는 특별한 의미가 없기 때문이었다.

 

 

 

하지만 버퍼 오버플로(Buffer Overflow)가 되는 순간에 사용자가 원하는 임의의 명령어를 수행시킬 수 있다는 가능성이 알려지면서 문제가 되기 시작했다.

 

 

 

특히 모리스 웜(Morris Worm) 사건 때 fingerd 데몬에서 ID를 입력받을 시에 버퍼 오버플로(Buffer Overflow)가 일어나는 것을 이용했다는 것이 알려지면서 문제의 심각성이 새삼 인식되기 시작하였다.

 

 

 

버퍼 오버플로(Buffer Overflow)를 이용하면 임의의 명령어를 수행시킬 수가 있는데, 이때 쉘(Shell)을 실행시킴으로써 임의의 작업을 수행시키는 것이 일반적이다.

 

 

 

그런데 심각한 것은 프로그램이 SETUID 루트(root) 프로그램인 경우에는 버퍼 오버플로(Buffer Overflow)를 이용해 일반 사용자가 루트 권한의 쉘을 얻을 수 있다는 것이다.

 

 

 

게다가 만약에 서버 프로그램이 버퍼 오버플로(Buffer Overflow)의 가능성을 가지고 있다면 외부 사용자가 내부 사용자 권한의 쉘도 얻을 수 있다.

 

 

 

일반적으로 서버 프로그램은 루트 권한으로 실행되기 때문에 이러한 문제는 더욱 심각해진다.

 

 

 

예전에는 시스템 내부에 대한 공격은 시스템 버그나 관리자의 실수, 잘못된 설정을 이용하는 공격이 대부분이었지만 최근 들어 거의 모든 해킹이 버퍼 오버플로를 이용한 공격이라고 해도 과언이 아닐 정도로 상당히 많이 쓰이고 있다.

 

 

 

버퍼 오버플로(Buffer Overflow) 공격은 컴퓨터가 프로그램을 실행시키는 과정과 그때 일어나는 메모리 관련 일 등에 대해서 자세하게 알고 있어야 그 동작 방식을 이해할 수 있는 아주 어려운 기술 중에 하나이다.

 

 

 

 

 

1997Aleph OnePhrack 잡지 49호에서 “Smashing the Stack for Fun and Profit"이라는 제목으로 글이 기고되고 나서, 가히 버퍼 오버플로(Buffer Overflow)의 시대라 할 만큼 모든 해킹이 이 방법을 통해 이루어지게 되었다.

 

 

 

 

 

프로그램의 구조적인 차원에서 한 번 보자. 다음 프로그램 소스를 주의 깊게 살펴보자

 

 

 

 

 

 

 

main()

{

printf("hello\n\n");

}

 

 

 

 

 

 

 

이것을 컴파일 해 보자. 당연히 hello라는 문자가 출력된다.

 

 

 

그러면 다른 소스를 보자.

 

 

 

 

 

 

 

int test(void)

{

int i=100;

i=i+1;

return i;

}

void main(void)

{

int k;

k = test(); /* (1) */

printf("정답(?) :: %d \n",k);

}

 

 

 

 

 

 

 

이 소스를 컴파일해서 실행하면 정답(?) :: 101” 이렇게 나온다.

 

 

 

프로그램의 흐름을 보자. 정답(?) :: 101 이라는 것을 출력하기 위해 프로그램은 이렇게 함수를 움직인다.

 

 

 

 

 

main() test() main() 종료

 

 

 

그런데 프로그램이 시작되면 메모리에 main() 함수 영역과 test() 함수 영역이 따로 구분되어 놓여져 있게 된다.

 

 

 

메모리가 잠시 옆으로 누워서 이런 형태로 보인다고 생각해 보자.

 

 

 

[ ]

 

 

 

그러면 main() 함수와 test() 함수가 메모리에 할당되어 메모리에 들어가 있을 때의 화면은 이렇게 된다.

 

 

 

 

 

 

 

 

 

[ test() main() ]

 

 

 

 

 

 

 

 

이제 한번 프로그램이 움직이는 순서대로 가보자. main() 함수가 호출되어 메모리에서 현재 프로그램이 움직이는 지점이 main() 이라는 부분일 테고, 곧바로 test()를 호출/* (1) */했기 때문에 현재 프로그램이 움직이는 지점이 test()라는 부분으로 움직인다.

 

 

 

그리고 test() 함수에서 일이 다 끝나면 다시 main() 함수로 움직이고 정답을 출력하고 프로그램이 종료된다.

 

 

 

그런데 여기서 프로그램이 움직이는 지점(포인터)main()으로 갔다가 test()로 갔다가 다시 main()으로 가는 일을 혼자서 스스로 움직이는 것은 불가능하다.

 

 

 

그래서 생각해낸 것이 메모리 부분에 주소(Address)를 할당하고 주소만 입력하면 그 주소를 곧바로 움직일 수가 있게 된다.

 

 

 

하지만 그래도 프로그램이 움직이는 포인터는 test()가 메모리 어디에 있는지도 모르고, main()도 어디에 있는지도 모르기 때문에 쓸모가 없다.

 

 

 

 

 

test()라는 함수가 메모리에 할당되기 전에 자신을 불렀던 main() 함수의 메모리 주소를 기억하고 있다.

 

 

 

그래서 프로그램이 움직이는 포인터가 test()로 갔다가 다시 main()으로 돌아가게 된다.

 

 

 

만약 그 저장된 리턴 어드레스가 엉뚱한 부분, main() 함수를 가리키지 않고 엉뚱한 곳을 가리킨다면 즉시 에러가 나면서 프로그램이 비정상적으로 실행되게 된다.

 

 

 

프로그램이 움직이는 포인터가 엉뚱한 리턴 어드레스로 메모리의 이상한 부분을 건드리게 되면 에러가 나는데 이게 바로 세그멘테이션 오류이다.

 

 

 

(Segmentation Fault)

 

 

그러므로 언제나 자신을 호출한 부모 함수의 주소를 리턴 어드레스가 생각하고 100% 안전하게 지키고 있어야 된다(test() 함수의 경우 부모 함수는 당연히 main() 함수가 된다). 리턴 어드레스가 왜 있는가에 대해서는 설명을 했고, 이번에는 test() 함수가 메모리에 할당되어 있는 모습을 좀 더 자세히 지켜보자. test() 함수가 메모리에 할당되어 있는 것을 확대시켜 보면

 

 

 

 

 

 

 

[ 변수에 관련된 것을 저장 리턴 어드레스(ret) ] <= test()

 

 

 

 

 

 

이런 형태로 있게 된다.

 

 

 

리턴 어드레스는 가장 뒤쪽에 놓여 있게 된다.

 

 

 

아까 test() 함수를 보면 int i; 라는 부분이 있는데 이것이 바로 변수를 설정하는 부분이다.

 

 

 

그래서 test()의 메모리는

 

 

 

 

 

 

 

[ (i 변수영역) ret(리턴 어드레스)]

된다.

 

 

 

 

그런데 i 변수가 점점 커지게 되면 이렇게 되게 된다.

 

 

 

 

[ (i 변수 영역) ret(리턴 어드레스)]

[ (i 변수 영역))) ret(리턴 어드레스)]

[ (i 변수 영역))))) ret(리턴 어드레스)]

[ (i 변수 영역))))))) ret(리턴 어드레스)]

[ (i 변수 영역))))))))) ret(리턴 어드레스)]

 

 

 

 

 

 

 

 

i 변수 영역이 계속 커지다 보니깐 리턴 어드레스 부분까지 계속 확장되게 된다.

 

 

 

여기서 주의 깊게 볼 부분은 i 변수 영역은 그대로인데 변수 안에 들어가는 내용이 점점 커져서 계속 부풀어 올라가는 모습이다.

 

 

 

이제 좀 더 커지게 되면 어떻게 될까? 리턴 어드레스 부분이 i 변수 내용이랑 겹치게 된다.

 

 

 

그렇게 되면 i 변수에 어떤 입력된 값이 들어가게 되면 리턴 어드레스 부분도 엉뚱한 숫자로 바뀌게 되어 원래 저장된 main() 함수의 주소를 잃어버리게 된다.

 

 

 

그러면 당연히 오류가 생기면서 Segmentation fault가 나고 프로그램이 죽게 된다.

 

 

 

 

 

이것이 바로 버퍼 오버플로가 일어난 예이다.

 

 

 

원래의 i 변수 영역에 들어가는 내용이 점점 커져서 리턴 어드레스까지 건드려 프로그램이 제대로 돌아가질 않게 되는 것이다.

 

 

 

해킹 기법도 바로 이런 점을 이용한 것이다.

 

 

 

이런 해킹 기법은 아까 말한 리턴 어드레스부분을 원하는 메모리 주소로 가게하고 그 부분에 엉뚱한 프로그램을 올려놓고 그것이 실행되도록 하는 것이다.

 

 

 

그전의 경우를 보면

 

 

 

 

 

 

 

main() -> test() -> 해커가 만든 함수 혹은 프로그램 부분

 

 

 

 

 

 

 

이렇게 되게 된다

관련자료

댓글 0
등록된 댓글이 없습니다.

공지사항


뉴스광장


  • 현재 회원수 :  60,043 명
  • 현재 강좌수 :  35,853 개
  • 현재 접속자 :  111 명