스택 버퍼 오버플로우를 이해하려면 버퍼 오버플로우에 대한 이해와 메모리 구조에 대한 이해가 필요하다.
그래서 메모리 구조와 버퍼 오버플로우에 대해 먼저 공부한 후 스택 버퍼 오버플로우를 공부해볼 것이다.
메모리 구조
- 코드 영역(Text)
프로그램을 실행시키기 위해 구성되는 것들이 저장되는 영역. 명령문, 제어문, 함수, 상수 등이 저장되는 영역이다.
- 데이터 영역(Data)
전역변수, 정적변수 등이 저장되는 영역. 초기화된 변수 영역(Initialized data segment)와 초기화되지 않은 변수 영역(Uninitialized data segment)로 나뉨.
- 힙 영역(Heap)
사용자에 의해 관리되는 영역으로 동적으로 할당된 변수들이 저장됨. 낮은 주소에서 높은 주소로 할당됨.
- 스택 영역(Stack)
함수를 호출할 때 지역변수, 매개변수들이 저장되는 영역. 메인함수 내의 변수들이 포함됨. 함수가 종료되면 함수에 할당된 변수들을 메모리에서 해제시킴. 높은 주소에서 낮은 주소로 할당됨.
버퍼 오버플로우(Buffer Overflow)
데이터의 길이를 명시하지 않아 메모리 영역을 넘치게 저장되는 것이다. 대표적으로는 Stack Buffer Overflow와 Heap Buffer Overflow가 있다. 이번에는 먼저 Stack Buffer Overflow를 알아볼 것이다.
스택 버퍼 오버플로우(Stack Buffer Overflow)
호출 스택이 할당된 스택 영역의 경계선 밖으로 넘어간 경우 발생한다. 예를 들면, 재귀 호출에서 발생할 수 있다.
#include <stdio.h>
int count = 1;
void func() {
int a = 1;
int b = 2;
printf("depth : %d\ta : %p\tb : %p\n", count, &a, &b);
count++;
func();
}
int main(int argc, const char * argv[]) {
func();
}
위의 함수 func()를 어셈블리어로 뜯어보면 다음과 같다.
0x100000f10 <+0>: pushq %rbp // 스택에 push
0x100000f11 <+1>: movq %rsp, %rbp // 첫번째 인자에 두번째 인자값 복사
0x100000f14 <+4>: subq $0x10, %rsp // 두번째 인자에 첫번째 인자만큼 뺄셈
0x100000f18 <+8>: movl $0x1, -0x4(%rbp)
0x100000f1f <+15>: movl $0x2, -0x8(%rbp)
0x100000f54 <+68>: callq 0x100000f10 ; <+0> at main.c:12
0x100000f59 <+73>: addq $0x10, %rsp
0x100000f5d <+77>: popq %rbp
0x100000f5e <+78>: retq
- rbp : 스택의 시작점을 가리킴.
- rsp : 스택의 마지막점을 가리킴. 즉, 스택의 꼭대기
쉽게 함수의 동작을 설명하자면,
1. 스택의 시작점을 스택에 push
2. 스택의 꼭대기 지점에 스택의 시작점의 인자값을 복사
3. 스택의 꼭대기 지점의 값에서 0x10(=16)만큼 뺀다.
4. 스택의 시작점에서 -0x4에 해당하는 부분에 0x1 값을 복사 (int a = 1;)
5. 스택의 시작점에서 -0x8에 해당하는 부분에 0x2 값을 복사 (int b = 2;)
6. 0x100000f10 지점으로 이동
7. 다시 0x100000f59 부분이 스택에 push (1번으로 돌아감)
처음 주소값(8byte) + 함수 내부 할당(16byte) + 다음 명령의 주소값(8byte) = 32byte
즉, 32byte만큼 스택에 쌓이는 구조이다.
실제로 코드를 실행시켜 보면 count++가 될 때마다 변수의 주소가 32씩 변화한다.
그림으로 보면 다음과 같다.
공격 절차
1. SetUID
: 일반 사용자가 root 권한으로 프로그램을 실행하는 것을 말한다.
보통 SetUID가 설정된 루트 권한이 있는 프로그램을 공격대상으로 한다. 스택에 정해진 버퍼보다 큰 공격 코드를 삽입해 반환 주소를 변경함으로써 공격 코드를 루트 권한으로 실행하게 하는 방법이다.
- Shell Code를 버퍼에 저장한다.
- 루트 권한으로 실행되는 프로그램의 특정 함수의 스택 반환 주소를 오버플로 시켜 Shell Code가 저장된 버퍼의 주소로 덮어쓴다.
- 특정 함수가 호출되면 Shell Code가 있는 반환 주소로 돌아가 Shell Code가 실행된다.
2. root 계정으로 test.c 파일을 만든다.
sudo su
vi test.c
3. ASLR을 해제하고 컴파일 옵션을 설정한다.
echo 0 > /proc/sys/kernel/randomize_va_space
/* 0 : ASLR 해제
* 1 : 랜덤 스택 & 랜덤 라이브러리
* 2 : 랜덤 스택 & 랜덤 라이브러리 & 랜덤 힙
*/
gcc -fno-stack-protector -z execstack -fno-builtin -mpreferred-stack-boundary=4 -o test test.c
/* -fno-stack-protector : stack-smashing protection 해제
* -z execstack : 스택 실행
* -fno-builtin : <string.h> 를 사용하지 않아도 strcpy()를 사용할 수 있게 함.
* -mpreferred-stack-boundary=4 : main을 포함해 이후 모든 ㅡ함수들의 스택 프레임에서
* 가장 낮은 주소가 2^n의 배수가 되도록 함.
*/
* ASLR에 대한 것은 메모리 보호 기법(ASLR)을 참고하면 된다.
* 참고로, stack-smashing protection은 스택 버퍼 오버플로우가 발생했을 때 보호하는 기법으로 canary라는 값을 이용해 발생 여부를 검증하는 것이다. 자세한 것은 Stack Smash Protection(SSP), 스택 보호 기법 카나리아(Canary)를 참고하면 된다.
4. chmod로 프로세스를 SetUID 권한으로 변경한다.
chmod 4755 test
ls -li | grep 'test*'
* grep : 리눅스에서 특정 파일에서 지정한 문자열이나 정규표현식을 포함한 행을 출력하는 명령
5. root 계정에서 일반 계정으로 변경하고 test 파일을 복사해 test2 파일을 생성한다.
sudo su
sudo cp test test2
6. gdb를 이용해 strcpy 함수의 실행 위치에 break를 걸어 실행한다.
gdb -q test2
7. strcpy 함수 실행 이후 buf[100]의 시작 위치를 확인한다.
x/100wx $rsp
x/100wx에 대해서는 GDB 기본 명령어를 참고하면 된다.
혹시나, 이 명령어를 실행 결과 "No Registers." 라고 뜬다면,
info registers
명령어를 입력해서 프로그램이 실행 중인지 확인해야 한다. "The program has no registers now." 라고 뜬다면 프로그램이 실행중인 상태가 아니므로, 실행시켜 주어야 한다.
run
명령어를 통해 프로그램을 실행시키고 다시 "info register"을 입력하면 레지스터 정보가 뜬다.
자세한 것은 어셈블리 언어 실습의 64페이지를 확인하면 된다.
- 0x00000001에서 strcpy()가 실행되고 0x00000000에서 buf[100]이 실행됨을 할 수 있다.
8. gdb에서 나와서 test 파일에 적용해 buf의 시작위치에서 Shell Code가 실행되도록 한다.
9. root 권한을 탈취했는지 확인한다.
다음은 Heap Buffer Overflow에 대해 작성해볼 것이다.
참고