진박사의 일상

[컴보] 10장 - Buffer Overflow 본문

프로그래밍/공부

[컴보] 10장 - Buffer Overflow

진박사. 2021. 11. 15. 18:59

Buffer Overflow

- 매우 흔한 Attack Mechanism(1998년 Morris Worm부터 널리 이용)

- 방지 기법이 잘 알려져 있음 -> 그럼에도 불구하고 OS 레거시 코드 또는 개발자의 부주의 때문에 여전히 문제

Buffer Overflow 역사

2004년까진 MS에 공격이 잦았으나 시큐어 코딩을 강조하고 자동 업데이트 덕분에 줄어들음 -> 대신 Flash나 Acrobat, 아래아한글 등 Application에 공격

 

Buffer Overflow/Buffer Overrun 정의

- 더 많은 input data를 buffer에 입력하여 그 주변의 데이터를 overwriting시키는 상태. 이를 이용하여 시스템을 crash시킬 수도 있고 해당 시스템을 조작하기 위해 설계된 코드를 삽입할 수도 있다.

- 기본 : 고정 사이즈 버퍼의 한계를 넘도록 데이터를 저장하려는 과정에서 생기는 프로그래밍 에러를 이용 -> 인접 메모리 위치를 overwrite(인접 메모리 위치는 프로그램 변수, 파라미터 또는 program control flow data(실행순서) 등이 저장되어 있을 수 있음), stack, heap, data section 등 버퍼가 있는 곳에서 발생할 수 있음.

- 결과 : 프로그램의 파괴, 의도치 않는 컨트롤로 전이, 메모리 접근 위반, 공격자가 삽입한 악성 코드 실행.

gets()는 버퍼 오버플로우 문제 때문에 현재 사용 x

8글자만 받아야 하는데 그 이상의 경우도 입력이 됨.

Buffer overflow Stack values

stack 영역의 buffer를 overflow시켜서 return address를 변경시키면 함수 종료 후 main으로 돌아가지 않고 다른 곳으로 이동할 수 있음.

 

Buffer Overflow Attacks

- 버퍼 오버플로우 공격을 하기 위해선 공격자가 외부 데이터를 입력하여 유발할 수 있는 버퍼 오버플로우 취약점이 있는 프로그램을 식별해야 함.

- 버퍼가 메모리에 어떻게 저장되는지 알아야 어떻게 파괴할지 정할 수 있음.

- 취약한 프로그램 식별 방법 : 프로그램 소스 검사, 다양한 입력을 시도해보며 추적, fuzzing(랜덤 입력값을 입력해보는 검사툴)을 이용하여 자동적으로 잠재적 취약 프로그램 식별

 

Programming Language History

- 컴퓨터 프로세서가 machine instructon을 실행하고 machine level의 data를 조작해 레지스터나 메모리에 저장함.

- 어셈블리 언어 프로그래머는 어디에 데이터를 저장하는게 옳은지 해석해야 함.

- 최근 high-level language는 strong notion of type과 valid operation을 통해 버퍼 오버플로우를 방지 -> 오버헤드 발생 <-> C와 같은 low-level language는 버퍼 오버플로우 취약점이 있음. 레거시에 C가 많이 쓰이므로 신경써줘야 함

 

Stack Buffer Overflow

- stack영역에 저장된 buffer에서 발생, stack smashing이라고도 함. Morris Worm에서 사용, 현재도 많이 사용됨.

- stack frame : 함수가 호출될 때마다 생성되어 return address와 parameter, local value 등이 저장됨.

 

Stack Frame

- P(param1, param2)에서 Q()를 호출한 상황

- Old Frame Pointer는 BP(Base Pointer)라고도 하며 EBP에 저장되어 Stack Frame의 시작 위치를 저장.

- Q()가 종료되면 Q의 스택 프레임은 접근할 수 없게 됨.

 

Program & Processes

프로그램이 실행되면 가상 메모리 상에 탑재

Space Memory를 사이에 두고 Stack과 Heap이 반대방향으로 메모리를 키워나가므로 만약 함수 호출을 너무 많이 하면 Heap 영역에 할당이 불가능해지고 반대로 Heap 영역에 할당을 많이 하면 함수 호출이 불가능해짐.

사용자 입력을 받는 부분에서 의도했던 길이의 입력을 넘어서면 Segmentation Fault가 발생 -> Overflow가 발생할 수 있다! (공격가능) -> perl 프로그램을 통해 헥사 코드로 인코딩하여 뒤쪽에 주소값을 주고 Pipe로 넘겨줌. 스크립트를 통해 호출된 원래 함수로 다시 보내줄 수 있게 됨. (Control Flow를 바꿀 수 있음) -> 목적하는 메모리 주소는 exec()와 같은 system call 함수 영역

 

stack 영역의 return addr 부분을 덮어씌우는 공격

또다른 예시

printf()의 "%d %s"와 같은 것이 정확히 몇 byte인지 확실하지 않아서 오버플로우가 발생할 가능성이 있음. -> snprintf()와 같은 함수를 이용해야 함.

버퍼 오버플로우에 취약한 C 함수들

 

Shellcode : 공격자가 제공한 코드 (버퍼 오버플로우된 버퍼에 저장됨) -> 주로 shell(command-line interpreter)으로 이동할 수 있도록 쓰여짐. 머신 코드로 작성됨.

- Machine Code : CPU와 OS에 의존적인 코드 -> assembly language skill로 작성 -> 자동화하는 tool이 존재. or C로 작성하고 나온 목적 파일 뜯어서 확인

- Metasploit Project : penetration이나 IDS signature 개발자, exploit research 등에 이용되는 유용한 정보 제공하는 자동화된 툴 (가상 공격 시도)

C로 만든 Shellcode를 compile -> assembly code를 추출 -> Hexacode로 변경(x86 machine code)

x86의 Assembly Language Instructions
x86의 레지스터들

%eax, %ebx, %ecx, %edx 는 연산할 때 / %ebp는 Base Pointer 주소 %eip는 Intruction Pointer(Program Counter) 주소 %esp는 Stack Pointer 주소를 저장하고 있음. ex) %eip를 변경시 다음 명령어의 주소가 달라져서 다른 명령을 할 수 있음.

 

실제 공격

-> Shellcode(90(NOP)으로 패딩을 줌)를 Pipe를 통해 입력값으로 buffer4 프로그램에 전달 -> 원래 root유저가 아니라 /etc/shadow 파일을 확인 불가능 -> buffer4의 set uid bit를 통해 임시적으로 root 권한을 받은 상태로 실행하는 상황에서 shell에 진입하게 되면 shadow를 확인 가능해짐.

 


Buffer Overflow - 2

 

target program : trusted system utility, network service daemon, commonly used library code 등에 취약점이 있을 경우

shellcode functions : remote shell을 실행, reverse shell connection을 생성, local exploits를 shell을 통해 사용, firewall rule을 삭제/무력화, chroot(root를 숨겨주는 기능)을 파훼

 

Buffer Overflow Defenses

- compile-time approach : 실행파일을 만들 때 공격을 막을 수 있게 만들자

- run-time approach : 공격이 확인되면 프로그램을 중단시키자

 

Compile-Time Defenses

- 현대 고급 언어(ex. JAVA 등)를 사용하기 : 컴파일러가 취약점이 될만한 range check를 함. (compile error 출력 등)

  - 단점 : 추가적인 코드랑 추가적인 리소스 사용 필요. machine language와 멀기 때문에 HW 자원 컨트롤 불가(C와 다르게)

- Safe Coding Techniques : 과거에는 space efficiency나 performance를 우선했다면 이제는 code safety를 고려. inspect 필요.

  - ex) OpenBSD project - 모든 code(OS, STDLIB, Common utilities)를 다 inspect하고 안전하지 않으면 x -> 가장 안전한 OS로 알려짐.

- Language Extensions / Safe Libraries : dynamic allocated memory를 다루는 새로운 라이브러리를 사용.

  - 단점 : 새로운 라이브러리로 모두 리컴파일링 필요, 서드파티 어플리케이션에선 사용 힘듦.

  - ex) Libsafe

- Stack Protection : stack의 진입과 퇴장 code에 stack의 corruption 여부를 체크하는 함수를 추가.

  - random canary를 사용 - unpredictable한 value일 필요가 있음. entry와 exit시의 값이 바뀌었는지 체크

  - RAD(Return Address Defender) - GCC extension으로 제공됨. stack 진입시 return address를 안전한 곳에 저장하고 exit시 return address가 저장된 값과 같은지 체크.

 

Run-Time Defenses

- Executable Address Space Protection : virtual memory의 일부 영역을 non-executable로 설정 (stack 영역은 데이터 저장하는 곳이므로 실행가능한 영역일 필요가 없으므로). MMU(Memory Managaement Unit)에서 지원해야 함. SPARC이나 Solaris에서 존재. Linux/Unix/Windows 에서도 설정 가능.

   - 문제 : 일부 예외 상황에서는 Stack에 실행가능한 Code가 저장될 필요가 있음.

- Address Space Layout Randomization(ASLR) : 프로세스 시작할 때마다 Stack, Heap, Global Data의 시작 주소(offset)의 위치를 랜덤하게 설정해줌. 최근 시스템은 주소 영역이 충분히 크므로 이로 인한 공간 낭비는 무시할만 함. heap buffer 주소랑 stdlib 주소도 다 랜덤.

- Guard Pages : critical regions 사이에 guard pages를 위치시켜 접근할 수 없게 함. 만약 이 영역을 읽으려고 한다면 process를 abort시킴.(MMU에 의해 불법적인 주소로 flag됨), stack frame이나 heap buffer 사이에 둔다.

 

Replacement Stack Frame

- saved frame pointer address를 덮어씌우는 변종 : dummy stack frame으로 덮어씌우는 공격방법

- off-by-one attacks : 1byte 정도만 에러를 발생시켜 공격.

- 방지책 : stack frame이나 return address의 변경을 detect, stack를 non-executable, randomization (기존 방법과 동일)

 

Return to System Call

- return address를 Standard Library Function으로 바꾸는 Stack Overflow 변종 : non-executable stack 방어에 대응하는 공격. STDLIB의 정확한 위치를 알고 있다면 사용 가능. <- 똑같은 방어 방법으로 대응 가능.

return address를 libraries의 주소로 변경

echo를 overflow 시켜서 return address를 library 주소로 바꿔 shell 실행하는 library 함수를 호출함. -> stack 영역을 non-executable하게 하더라도 library 영역을 non-executable하게 할 수 없으므로 대응 불가.

 

Heap Overflow

- heap에 위치한 buffer를 공격 - dynamic data structure로 사용되므로 return address가 없음. -> control 이전이 어렵다. -> function pointer를 악용 가능 -> 대응 : heap을 non-executable하게, heap memory 할당을 랜덤하게.

heap overflow 예시

함수포인터를 heap 영역에 동적할당 시키고 해당 함수포인터에 buffer overflow 취약점이 있는 함수가 할당된다면 heap overflow를 유도할 수 있음.

 

Global Data(전역변수 영역) Overflow

- Heap Overflow와 비슷하게 전역 변수에 함수포인터를 할당해서 공격

global data overflow 예시

 

Return-Oriented Programming(ROP)

- 악성 코드를 추가하지 X, 라이브러리 함수 호출 X, 원본 코드 수정 X but Stack의 내용만 변경 가능하다면 공격 ok

- 거의 대부분의 시스템에서 공격 가능.

- 아이디어 : arbitrary comutation(임의 계산)을 함.

- 접근방법 : 작은 명령 시퀀스를 이용. 2~5개의 Instruction sequence를 모아서 gadget(시퀀스의 마지막은 항상 RET instruction)을 생성 -> 여러개의 gadget을 합치면 공격자가 원하는 공격을 할 수 있음.

gadget의 예시

대부분의 시스템이 가지고 있는 libc 라이브러리에서 다음과 같은 instruction이 있을 때 명령어 해독을 처음부터 하는 것이 아니라 중간부터 하게 되면 전혀 다른 명령어로 해석되게 됨. -> 그 중에서 마지막이 ret으로 해석되는 sequence를 여러개 찾아서 gadget으로 설정.

 

메모리 로드 과정

(1) stack overflow를 일으켜 return address를 pop %eax를 실행하는 gadget의 위치로 변경

(2) pop %eax의 return address를 그 다음 gadget(mov1 64(%eax), %eax)의 위치로 설정. 

(3) eax의 값에 64를 더한 위치에 있는 값을 eax에 저장하라 라는 gadget의 명령어 수행

(4) 목적 달성

 

대응 방법

- 요청한 함수의 return수와 실제 수행된 return의 수가 차이가 발생하면 이상으로 탐지 - but, 공격자가 return수를 맞춘다면? -> ROP without Returns

 

ROP without Returns

- 이미 튜링완전한 gadget set이 발견되어 있기 때문에 return 대신 return-like sequences(ex. JMP) 를 사용하면 됨.

 

'프로그래밍 > 공부' 카테고리의 다른 글

[데베시] 10강  (0) 2021.11.24
[데베시] 9장 복잡한 SQL  (0) 2021.11.20
[데베시] 8장 - SQL  (0) 2021.11.04
[컴보] 8장 IDS  (0) 2021.11.03
[컴보] 7장 DoS  (0) 2021.11.01