티스토리 뷰

[버퍼 오버플로우의 개념과 방법]

 

버퍼 오버플로우는 시스템 해킹의 대표적인 공격 방법 중 하나이다. 버퍼(Buffer)라는 것은 보통 데이터가 저장되는 메모리 공간을 일컫는데 단순히 메인 메모리만이 아닌 다른 하드웨어에서 사용하는 임시 저장 공간 역시 버퍼라고 부른다. 오버플로우(Overflow)는 단어 뜻에서 유추할 수 있듯이 데이터가 지정된 크기의 공간보다 커서 해당 메모리 공간을 벗어 나는 경우 사용한다. 결론적으로 버퍼 공간의 크기보다 큰 데이터를 저장하게 해서 일어나는 오버플로우(Overflow)를 이용한 공격이다. 보통 개발된 프로그래밍 언어의 버퍼 오버플로우에 취약한 함수가 있는 경우 이를 이용해 공격을 시도하며 공격이 성공할 경우 시스템의 권한을 상승시키거나 악성 행위를 하도록 할 수 있기 때문에 반드시 이 공격에 대한 방어를 철저히 해야한다.

 

우선 간단하게 버퍼 오버플로우 공격을 실습 해 보도록 한다.

[그림 1]

 

[그림 2]

 

[그림 1]과 같이 코딩을 하고 실행시키면 [그림 2]와 같이 실행이 된다. [그림 1]을 보면 func1 함수와 func2 함수가 있는데 main 함수에서는 scanf 함수로 buffer 배열에 문자열을 저장하고 func1 함수만 실행을 시킨다. 여기서 우리가 하려고 하는 것은 func2를 강제로 실행되게 하는 것이다. 우선 우리는 프로그램이 실행될 때 메모리에 할당되는 공간의 크기를 알고 있어야지 정확한 공격이 가능하기 때문에 gdb를 이용해 프로그램을 분석해 보도록 한다.

 

[그림 3]

 

[그림 3]은 gdb로 방금 코딩한 프로그램을 열어본 화면인데 <main+19> 부분을 보면 scanf 함수에 인자값으로[ebp-24] 의 주소값을 전달하는 것을 확인할 수 있다.

 

[그림 4]

 

간단하게 스택을 그려보면 [그림 4]와 같은데 EBP 위에 24 byte 만큼의 메모리 공간이 할당되고, 우리가 입력한 데이터가 저장된다는 것을 알 수 있다. 스택에서 배열이 할당되면 가장 낮은 주소부터 0번 인덱스가 시작된다. 그럼 우리는 데이터를 24byte 보다 더 많은 값을 전달해 RET가 다른 값으로 덮어 써지도록 해야한다.

 

[그림 5]

 

실제로 입력할 때 데이터를 28개를 입력해서 전달하면 'Segmentation Fault' 가 일어나는 것을 확인할 수 있다. 24개 까지는 입력해도 오류가 발생하지 않지만 4개를 더 입력하게 되면 백업해둔 EBP 값이 덮어 씌어지기 때문에 이런 오류가 발생하게 되었다. 그럼 이를 이용해서 우리는 RET 영역에 func2 함수의 주소값을 입력하면 될 것이라는 것을 추측 할 수 있다. 왜냐하면 RET 값은 함수가 종료되고 난 다음에 실행할 명령어의 주소값이 입력되어 있기 때문에 이 곳에 func2 함수의 주소값이 있다면 함수가 종료되고 func2 함수가 실행된다.

 

[그림 6]

 

gdb를 이용해서 프로그램을 연 다음 'info function' 명령어를 실행시키면 프로그램에 있는 함수들의 목록과 주소값을 출력 해 주는데 여기서 우리는 우리가 원하는 func2 함수의 주소값을 확인할 수 있다.

이제 이렇게 찾은 함수의 주소값을 RET 위치에 덮어 써야하는데 A를 28번 입력하고 이어서 주소값을 넣어주면 RET 위치에 정확히 덮어 써진 다. 왜냐하면 버퍼 공간이 24byte 만큼 할당되어 있고, 이전 EBP 값이 4byte 만큼 있으니 데이터를 28byte 만큼 써주면 EBP 까지 덮어써진다. 그리고 이어서 함수 주소값을 넣어주면 RET 영역이 해당 주소값으로 덮어 써진다.

 

EBP 까지 덮는건 그냥 'A' 와 같이 아무 값이나 입력해서 전달하면 되고 문제는 주소값을 어떻게 문자열로 전달을 하느냐가 관건이다. AAAAAAAAAAAAAAAAAAAAAAAAAAAA\x79\x83\x04\x08 이렇게 입력을 하면 우리는 \x79 가 하나의 값으로써 전달되어야 하는데 \ x 7 9 가 따로 따로 문자로 취급이 되어서 전달이 되기 때문이다.

 

※ \x79 는 79라는 값을 16진수 값으로 인식하겠다는 말이다.

※ 주소값을 2자리씩 끊은 것은 일단 메모리가 byte 단위로 저장 공간이 분할 되어 있기 때문이다. 16진수에서 1byte에 해당하는 자리수는 00 부터 FF 까지이다.

※ 주소값을 거꾸로 입력한 이유는 cpu가 리틀 엔디언(Little-endian) 방식으로 데이터를 처리하기 때문이다. 데이터 처리 방식이 크게 빅 엔디언(Big-endian)과 리틀 엔디언(Little-endian) 으로 나뉘는데 빅 엔디언은 사람이 읽기 편한 방식으로 앞자리부터 읽는 방식이고 리틀 엔디언은 컴퓨터가 읽기 편하게 뒷자리부터 읽는 방식이다. 이 두 방식의 차이점과 장단점은 나중에 따로 정리하기로 한다.

 

그래서 우리가 생각할 수 있는 방법은 \x79 값에 해당하는 문자를 입력해서 전달하는 것이다. 그런데 이 방법 역시 아스키 코드에 범위를 벗어나는 값이 있다면 사용할 수 없는 방법이다. 결론적으로 간단한 코딩을 통해서 이 문제를 해결할 수 있다.

[그림 7]

 

[그림 7]과 같이 코딩을 하면 되는데 간단하게 살펴보면 buffer의 28byte 만큼은 65를 넣고 그 다음 4byte 만큼은 사용자에게 입력 받는 값이 저장된다. 그리고 fwrite 함수를 이용해 32byte 만큼을 attack.txt 파일에 저장하게 된다.

 

이렇게 코딩을 한 이유는 아스키 코드의 범위를 넘어서는 값에 대해서 파일에 써주기 위함이다. 이 프로그램을 실행시켜서 아스키 코드 범위를 벗어나는 값을 입력하면 비록 문자가 출력이 되지는 않으나 해당 값이 파일에 써진다. 이렇게 만든 파일을 우리는 공격하려는 프로그램에 전달하게 되면 우리가 원하는데로 동작하게 된다.

[그림 8]

 

[그림 9]

 

방금 코딩했던 프로그램을 실행 시켜서 121, 131, 4, 8 을 순서대로 입력한다. (해당 값들은 각각 \x79, \x83, \x04, \x08 을 10진수로 변환한 값이다.) 그리고 attack.txt 를 vi로 열어보면 [그림 9]와 같이 출력이 되는데 제일 끝에 있는 이상한 값들은 모두 우리가 입력했던 값들이 특수 기호로 나타나진 것들이다.

[그림 10]

 

[그림 9]에서 나온 값을 공격 하려는 프로그램에 전달하면 프로그램의 스택 프레임이 [그림 11]과 같이 될 것을 예상할 수 있다.

[그림 11]

 

attack.txt 를 프로그램에 입력값으로 전달하게 되면 [그림 11]과 같이 func2 가 호출되면서 Success!!!가 출력 된다.

 

여기 까지가 기본적으로 버퍼 오버플로우 공격의 원리에 대해 알아 보았고 이제는 실제로 공격을 이용해 시스템의 상위 권한을 얻는 방법에 대해 알아보도록 한다.

 

공격 방법은 여러 가지가 있는데 여기서는 NOP Sled(NOP 썰매) 기법과 환경변수를 이용한 방법 이렇게 대표적인 2가지를 소개하겠다.

 

1. NOP Sled (NOP 썰매)

이름하야 NOP 썰매기법이라는 공격인데 이름이 인상적이다. 공격 개요를 간단하게 묘사해보면 쉘을 실행시키는 쉘 코드를 앞에 NOP 명령어를 다량 붙여서 메모리에 저장시켜두고, 버퍼 오버플로우 공격을 이용해 스택프레임의 RET 값을 쉘 코드가 있는 곳의 메모리 주소로 덮어씌워 주면 프로그램이 NOP을 따라 실행되다가 최종적으로 쉘 코드를 실행하게 된다. 여기서 프로그램이 NOP을 타고 쉘 코드 까지 내려온다고 해서 NOP Sled (NOP 썰매) 기법이라고 이름이 붙여졌다.

 

[그림 12]

 

NOP Sled 의 원리를 살펴보면 우선 쉘 코드를 버퍼에 저장시키고 RET에는 쉘 코드가 있는 곳의 주소를 넣으면 되는데 스택의 주소는 계속해서 바뀌기 때문에 앞에 NOP을 다량 붙여두고 RET에는 임의의 주소값을 넣고 실행시키다 보면 RET가 가리키는 주소가 NOP이 있는 곳의 주소를 가리키게 되고 최종적으로 쉘 코드가 실행되게 하는 것이다.

 

[그림 13]

 

우선 실습을 위한 취약한 프로그램부터 코딩을 해 보면 [그림 13]과 같이 할 수 있다. buffer 배열에 사용자 입력값을 복사하고 출력하는 프로그램이다. root 게정에서 진행 해야하며 컴파일 한 뒤 반드시 4755 와 같이 setuid를 걸어줘야한 취약한 프로그램이 완성된다. 이제 본격적으로 버퍼 오버플로우를 이용해 권한을 상승시켜 보겠다. 여기서부터는 낮은 계정으로 진행하면 된다.

 

[그림 14]

 

우선 buffer 배열이 메모리에 얼마만큼의 크기로 할당이 되었는지 확인을 하기 위해 gdb로 프로그램을 열어 보았다. strcpy 의 인자값으로 [ebp-520]의 주소값이 전달되는 것으로 보아 buffer 배열의 시작점은 [ebp-520] 인 것을 확인할 수 있다.

그렇다면 NOP + ShellCode 를 524byte 만큼 넣어주고, 4byte 를 임의의 esp 주소값으로 덮어 쓰면 공격에 성공할 수 있다. 물론 이 경우 esp 주소가 계속 바뀌기 때문에 여러번 시도해 보아야 한다.

 

[그림 15]

 

esp 값을 대략적으로 추정하기 위해 현재 esp 값이 뭔지 알야아 한다. 따라서 [그림 15]와 같이 코딩을 하고 실행을 시키면 현재 esp 값이 출력이 된다.

 

※ sp 함수에 return 이 없더라도 eax 레지스터가 본래 return 값이 저장되는 레지스터이기 때문에 정상적으로 동작이 된다.

 

[그림 16]

 

그리고 [그림 16]과 같이 실행을 시키다보면 어느 순간 /bin/sh 가 실행되면서 공격에 성공하게 된다. 명령어를 분석 해 보면 ./nop 의 인자 값으로 어떤 문자열들이 전달되고 있는데 이 문자열은 perl 이라는 스크립트 언어로 작성이 되었다.

 

`perl –e 'print "\x90"x463," \x31\xc0\xb0\x31\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\x46\xcd\x80\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\x2f\x62\x69\x6e\x2f\x73\x68","\x38\xef\xff\xbf'`

위 코드에서 perl –e 는 커맨드 라인에서 바로 실행하겠다는 의미로 따로 파일로 작성하지 않고 바로 실행하도록 하기 위해서 –e 옵션을 주었다. \x90 은 16진수로 NOP 에 대한 코드이며 x463은 463번 출력을 하겠다는 의미이다. 이어서 콤마(,)로 다음 문자열을 이어서 입력할 수 있는데 우리는 NOP과 쉘 코드를 함께 입력해주어야 하므로 쉘 코드를 넣어주면 된다. 반드시 쌍따옴표(") 로 묶어줘야 한다. 그리고 이어서 우리가 앞에서 출력했던 esp 값을 넣어주면 공격 코드가 완성된다. 따라서 `perl –e 'print "<NOP>"x<개수>, "<ShellCode>","<ESP주소>"'` 로 입력 해 주면 된다. 바로 공격에 성공하기는 힘들기 때문에 계속해서 될 때 까지 실행을 시키다 보면 공격에 성공하게 된다.

 

2. 환경 변수를 이용한 방법

 

앞에서 본 NOP Sled 는 공격이 한번에 이루어지지 않고 buffer의 메모리 공간이 최소 61바이트 이상(쉘 코드의 크기가 61byte 였다) 이어야만 공격이 가능했다. 이번에는 환경 변수를 이용해서 좀 더 쉽게 공격을 수행하는 법에 대해 알아 보도록 한다.

 

환경 변수란 시스템에서 사용하는 변수로 시스템의 설정이나 명령어의 경로 등이 저장되어 있다.

 

[그림 17]

 

이번에는 [그림 17]과 같이 프로그램을 작성해보자. buffer 의 크기가 20byte로 줄어들었다. 컴파일 후 마찬가지로 4755 의 권한을 주어야만 공격 실습이 가능하다.

 

[그림 18]

 

마찬가지로 gdb를 이용해 열어보면 이번에는 strcpy의 인자값으로 [ebp-40]의 주소값이 전달되었다. 우리가 앞에서 썼던 쉘 코드는 61byte기 때문에 프로그램에 온전히 전달할 수 없다.

 

[그림 19]

 

일반 사용자 계정에서 export 명령어를 이용해서 환경 변수에 쉘 코드를 추가하는 작업이 [그림 19]에서 이루어 지고 있다. export [변수명]=값 형태로 입력을 하면 된다. 앞에서 NOP Sled 때와 다른것은 NOP의 값이 100번 들어갔고, 쉘 코드 다음에 esp 주소값이 없어졌다. 이번에는 단순히 환경변수에 쉘 코드만 추가하면 되기 때문에 당연하다.

 

※ 계속해서 perl을 쓰는 이유는 우리가 NOP을 일일이 입력하기 힘들기 때문이다.

※ NOP 을 또 추가해준 이유는 환경 변수의 주소값 역시 조금씩 변한다. 그래서 좀 더 공격의 확률을 높이기 위해 NOP을 넣어 주었다.

 

[그림 20]

 

[그림 20]과 같이 코딩을 하면 우리가 원하는 환경 변수의 주소값을 출력할 수 있다.

 

[그림 21]

 

프로그램 명과 우리가 방금 등록했던 환경변수 이름을 전달하면 [그림 21]과 같이 환경 변수의 주소값이 출력된다.

 

[그림 22]

 

이번에는 [ebp-40] 부터 데이터가 입력 되므로 dummy 값을 앞에 44byte 만큼 넣어주고 이어서 환경 변수의 주소값을 입력해주면 [그림 22]와 같이 한번에 공격이 성공한 것을 확인할 수 있다.

 

신고
크리에이티브 커먼즈 라이선스
Creative Commons License
댓글
댓글쓰기 폼