티스토리 뷰
환경
환경은 아래와 같습니다.
OS: Kali Linux 1.1 0a 32 bit
생성하게 될 파일은 아래와 같습니다
buf.c, eggshell.c, getegg.c
스택 버퍼 오버플로우 공격 과정
아래 코드는 버퍼 오버플로우 공격에 취약한 buf.c로 버퍼는 char 64바이트, foo 함수가 매개변수로 argv[1]을 받아 bar 함수에게 넘겨주고, bar 함수 안에서 버퍼 오버플로우 공격에 취약한 strcpy 함수를 사용하여 buf 배열에 argv 값을 할당해 버퍼 오버플로우를 발생시킵니다.
명령어: gcc -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack -g -o buf buf.c
-fno-stack-protector 플래그로 스택 보호 기법을 해제하고, -mpreferred-stack-boundary=2 플래그로 스택 더미를 제거, -g 플래그로 디버거를 사용할 수 있게하고 -o 플래그로 실행 파일 이름을 buf로 설정합니다. 마지막으로 쉘 코드 실행을 위해 -z execstack 플래그로 스택 메모리에 실행 권한을 부여합니다.
명령어: echo 0 > /proc/sys/kernel/randomize_va_space
현재 리눅스 기반 운영체제에서는 기본적으로 스택 오버플로우와 같은 메모리 공격을 방지하기 위해 프로그램 실행 시 마다 주소 공간 배치를 무작위로 섞는 ASLR(Address Space Layout Randomization)기법을 사용합니다. 따라서 /proc/sys/kernel/randomize_va_space 파일은 default 값으로 랜덤 스택, 랜덤 라이브러리, 랜덤 힙 설정을 의미하는 2가 저장되어 있습니다. 1은 랜덤 스택, 랜덤 라이브러리를 의미하고 0은 주소 공간 배치를 무작위로 섞지 않고 고정하는 것을 의미합니다. 따라서 직접 해당 파일에 가서 입력해주거나 echo 0 > filename 명령어로 0을 적어줍니다.
명령어: gdb -q buf
gdb 디버거를 -q 플래그와 buf 실행 파일 명과 함께 실행합니다.
-q는 quiet의 약자로 불필요한 아웃풋들을 출력하지 않게끔 합니다.
명령어: break 13
buf.c 코드를 보면 13번째 줄에서 foo() 함수를 호출합니다. 따라서 breakpoint로 13번째 줄을 지정합니다.
명령어: run `python -c ‘print “A”*24 + “B”*44 + “C”*4’`
Python 코드를 통해 매개변수로 “A”*24 + “B”*44 + “C”*4 문자열을 넘겨줍니다. 지정한 버퍼의 크기가 64이므로 64바이트 + 4바이트(프레임 포인터를 위한 공간의 크기) + 4바이트(복귀 주소를 위한 공간의 크기) 총 72바이트가 사용됩니다. 이 중 24바이트는 “A”, 44바이트는 복귀 주소까지의 공간을 채우기 위해 “B”, 마지막 4바이트는 복귀 주소로 “C”의 값이 할당됩니다. 이후 24바이트는 쉘 코드를 위해 사용되고, 44바이트는 쓰레기 값, 마지막 4바이트는 쉘 코드의 주소값으로 사용됩니다.
명령어: step
프로그램을 한 줄씩 실행하며, 함수가 있으면 함수 내부로 이동합니다. breakpoint로 지정한 13번째 줄에 foo() 함수가 있으므로 함수 내부로 이동합니다. 아래 이미지에 이동한 결과 bar() 함수가 있는 9번째 줄로 이동한 것을 볼 수 있습니다.
명령어: step
마찬가지로 bar() 함수 내부로 이동하기 위해 사용합니다. 아래 이미지에 이동한 결과 strcpy() 함수가 있는 6번째 줄로 이동한 것을 볼 수 있습니다.
명령어: next
프로그램을 한 줄씩 실행하기 위해 사용합니다. 함수가 있으면 함수를 실행한 후 다음 줄로 이동합니다. 그 결과 7번째 줄로 이동한 것을 볼 수 있습니다.
명령어: x/20x
맨 앞의 x는 examine의 약자로 gdb에서 메모리를 조사하기 위해 사용합니다. 그 다음 /와 함께 맨뒤에서 부터 x는 16진법으로 보여준다는 것을 의미하고, 20은 4바이트 단위로 총 20개를 보여줍니다. esp부터 시작해서 높은 주소의 방향으로 보여줍니다.
명령어: continue
현재 위치에서 프로그램의 끝이나 다음 브레이크 포인트까지 실행하는 명령어입니다. 실행하면 복귀 주소인 0xbfffeaf4로 이동하고 그 값인 0x43434343으로 가서 실행하려고 하면 Segmentation fault가 발생합니다. 이제 버퍼 시작 주소가 0xbfffeab0인 것을 알았으니 이 곳에 쉘 코드, 이후 44 바이트는 쓰레기 값, 마지막 복귀 주소가 담겨있는 곳에 쉘 코드의 시작 주소를 저장시키고 실행하면 쉘 코드가 실행할 것입니다.
이제 버퍼의 시작 주소를 파악했으니 버퍼 오버플로우 공격을 진행합니다. 아래 두 줄 gdb -q buf, break 13은 위와 같습니다.
명령어: run `python -c ‘print “24바이트 쉘코드 명령어” + “44바이트 쓰레기 값” + “쉘 코드 주소”’ `
아래의 쉘 코드는 쉘을 실행하는 코드이고 총 24바이트로 되어 있습니다. 이후 쉘 코드 주소 직전까지의 공간을 채우기 위해 총 44바이트의 쓰레기 값으로 “A”를 지정해줍니다. 마지막으로 쉘 코드 주소로 이전에 알아낸 0xbfffeab0를 지정해줍니다. 16진법과 리틀 엔디언 방식으로 거꾸로 적어주면 \xb0\xea\xff\xbf가 됩니다.
위에서 했던 것과 마찬가지로 foo(), bar() 함수 내부로 이동하기 위해 step 명령어를 2번 사용합니다. 이후 strcpy() 함수를 실행하기 위해 next 명령어를 입력합니다.
위에서 설명했던 x/20x 명령어를 사용해 esp를 시작으로 높은 주소 방향으로 20 * 4 바이트의 메모리를 조사합니다. 아래 이미지에서 볼 수 있듯이 0xbfffeaab0 주소에 0x6850c031로 시작하는 쉘코드가 저장되어있는 것을 알 수 있습니다. 또한 0x41(“A”값)이 44바이트 만큼 저장되어 있는 것을 확인할 수 있고, 마지막으로 복귀 주소 공간에 쉘 코드의 시작 주소인 0xbfffeaab0가 저장되어 있는 것을 알 수 있습니다. 따라서 최종적으로 복귀 주소를 쉘 코드의 시작 주소로 덮어쓴 상태로, 함수가 종료될 떄 쉘 코드가 실행되게 됩니다. (3.스택 구성 참고)
continue 명령어를 입력해주면 함수가 종료되고, 쉘 코드가 실행됩니다. 그 결과로 # 커서가 잡힌 것을 볼 수 있습니다.
Eggshell을 이용한 스택 버퍼 오버플로우
만약 버퍼의 크기가 쉘 코드를 삽입하기에 충분하지 않다면(예를 들어 위에서 사용한 쉘 코드인24바이트 이하) eggshell을 사용할 수 있습니다. Eggshell은 메모리에 쉡 코드를 등록해놓고 등록된 주소를 반환해줍니다. 이 주소를 복귀 주소에 저장하면 쉘 코드를 실행할 수 있게 됩니다. 코드 작성 후 아래와 같은 명령어로 컴파일합니다.
이후 각 실행 파일을 순서대로 실행합니다. 쉘 코드가 등록된 주소는 0xbfffee65 이므로 매개 변수에 버퍼를 쓰레기 값으로 채운 후 해당 주소를 넘겨 쉘 코드가 실행되게끔 할 수 있습니다.
아래의 코드처럼 버퍼 64바이트와 이전 프레임 포인터 4바이터를 합친 68바이트를 쓰레기 값으로 채운 후 쉘 코드의 주소를 채우고 프로그램을 실행하면 쉘 코드가 실행되어 쉘 프로그램을 사용할 수 있는 것을 확인 할 수 있습니다.
스택 구성
foo 함수가 bar 함수를 호출했을 때의 스택의 구성은 아래와 같습니다. Bar 함수를 호출할 때 foo() 내에서의 복귀 주소를 스택에 넣고 foo 스택 프레임의 시작점을 스택에 넣습니다. 스택 포인터는 bar 스택 프레임의 이전 프레임 포인터를 가리킵니다. 이후 프레임 포인터를 스택 포인터 값으로 설정하면 프레임 bar의 시작점을 가리키게 됩니다. 함수 파라미터와 지역 변수를 넣습니다. Main에서 foo함수를 호출할 때도 같은 방식으로 이루어집니다.
bar 함수가 수행이 완료되거나 return을 만나면 스택 포인터를 프레임 포인터 값으로 바꿉니다. 이렇게되면 파라미터와 지역 변수에 할당된 공간을 운영체제에 반환하게 됩니다. 이후 bar의 이전 프레임 포인터의 주소값을 프레임 포인터에 설정합니다. 이렇게 하면 프레임 포인터는 foo 스택 프레임의 시작점을 가리키게 됩니다. 이와 동시에 스택 포인터는 foo 내에서의 복귀 주소 상단을 가리키게 됩니다. foo가 반환될 때도 같은 방식으로 이루어집니다.
아래의 스택 구성은 버퍼 오버플로우 공격을 당한 이후의 스택의 상태로, 기존의 버퍼가 차지하고 있는 공간에 쉘 코드 24바이트와 쓰레기 값 문자 “A” 44바이트(이 중 마지막 4바이트는 이전 프레임 포인터 영역을 침범)가 할당이 되어 있습니다. 그 위로 기존에 복귀 주소가 저장되어 있는 공간에 쉘 코드의 시작 주소가 저장이 되어 있습니다. 때문에 bar 함수가 종료될 때 쉘 코드가 실행됩니다.
방어 기법
1. SSP(Stack Smashing Protector)
SSP는 스택 오버플로우 공격을 막기 위한 기법으로 gcc 4.1 버전부터 컴파일 옵션으로 지정할 수 있습니다.
위의 코드처럼 버퍼 오버플로우가 발생하는 프로그램을 gcc 4.1 이상의 버전으로 컴파일 후 프로그램을 실행시키면 아래와 같은 문구가 표시됩니다.
컴파일 시 -fno-stack-protector 플래그를 지정하여 SSP 설정을 해제하였으나 기본적으로 gcc 4.1 이상부터는 SSP 설정이 되어 있습니다. SSP의 기능으로는 로컬 변수 재배치, 로컬 변수 전 포인터 배치, canary 삽입이 있습니다. 로컬 변수 재배치는 위의 예의 buf[] 변수와 같은 char 배열을 스택 상단에 배치시켜 값이 변조되는 것을 막습니다. 로컬 변수 전 포인터 배치는 아래와 같이 포인터 변수가 매개변수로 존재할 때, buf를 오버플로우시켜 p의 값을 변조하는 것이 가능할텐데, 이를 막기 위해 스택 상에서 해당 포인터 변수를 buf 아래에 두어 변조할 수 없게끔 합니다.
마지막으로 canary 삽입은 함수 호출 후 스택의 아래와 같은 구조에서 buf와 같은 변수의 공간과 ebp 사이의 canary라고 하는 값을 두어 함수 종료 시점에 canary의 값이 바뀌었는지의 여부로 버퍼 오버플로우를 탐지합니다. 만약 변조되었다면 프로그램을 종료합니다. Canary가 변조된 것으로 판단되어 프로그램이 종료된 경우 stack smashing detected 메시지를 표시합니다.
2. ASLR(Address Space Layout Randomization)
메모리 공격을 방어하기 위해 주소 공간 배치를 난수화하는 것으로 실행 시 마다 메모리 주소를 변경시켜 버퍼 오버플로우를 통한 특정 주소 호출(쉘 코드)을 차단합니다. 현재 리눅스 기반 운영체제에서는 기본적으로 /proc/sys/kernel/randomize_va_space 파일 default 값으로 랜덤 스택, 랜덤 라이브러리, 랜덤 힙 설정을 의미하는 2가 저장되어 있습니다. 1은 랜덤 스택, 랜덤 라이브러리를 의미하고 0은 주소 공간 배치를 무작위로 섞지 않고 고정하는 것을 의미합니다. 리눅스에서 아래와 같은 코드로 설정할 수 있습니다.
결론
위에서 방어 기법이라고 소개한 방법들도 사실 가장 최근 버전 리눅스와 gcc에서는 기본 값으로 설정되어 있습니다. 따라서 스택 오버플로우를 막기 위해 실습에서 gcc 설정으로 지정한 -fno-stack-protector와 리눅스 운영체제에서 echo 0 > /proc/sys/kernel/randomize_va_space 명령어 등은 굳이 지정하지 않아도 됩니다. 하지만 여전히 C, C++와 같이 개발자가 malloc, free/new, delete와 같은 함수로 메모리 관리를 직접 해야하는 프로그래밍 언어를 사용할 때는 신경쓰지 않으면 버퍼 오버플로우 공격에 노출될 수 있습니다. 프로그램 실행 시에 스택이 어떻게 구성되는지 등과 같은 메모리에 대한 지식이 있어야 여러가지 보안 공격에 사전에 대비할 수 있습니다. 또한 C에서 버퍼 오버플로우에 취약하다고 알려진 strcpy(), gets(), scanf() 등의 함수를 무심코 사용하는 경우가 그렇습니다. C에서 위와 같은 기능이 필요할 때는 경계값을 지정해 오버플로우를 제한하는 strncpy() 등과 같은 함수를 사용해야 합니다.
'Etc.' 카테고리의 다른 글
#Review 나는 LINE 개발자입니다 (0) | 2021.10.17 |
---|---|
컴파일러와 인터프리터의 차이 (0) | 2021.10.10 |
C의 union은 무엇이고 언제 사용할까? (0) | 2021.10.03 |
<DCinside> 해외주식갤러리 ticker별 언급 횟수 (2021-02-04~) (0) | 2021.02.05 |
<DCinside> 해외주식갤러리 ticker별 언급 횟수 (2021-01-15~) (1) | 2021.02.02 |